fix(editor): Properly update workflow info in main header (#9789)

This commit is contained in:
Csaba Tuncsik
2024-06-24 15:08:24 +02:00
committed by GitHub
parent 44ea4c73eb
commit 1ba656ef4a
9 changed files with 789 additions and 710 deletions

View File

@@ -1,8 +1,14 @@
import { ref, nextTick } from 'vue';
import { useRoute } from 'vue-router';
import { v4 as uuid } from 'uuid';
import type { Connection, ConnectionDetachedParams } from '@jsplumb/core';
import { useHistoryStore } from '@/stores/history.store';
import {
CUSTOM_API_CALL_KEY,
FORM_TRIGGER_NODE_TYPE,
NODE_OUTPUT_DEFAULT_KEY,
PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
WEBHOOK_NODE_TYPE,
} from '@/constants';
import { NodeHelpers, NodeConnectionType, ExpressionEvaluatorProxy } from 'n8n-workflow';
@@ -26,6 +32,10 @@ import type {
INodeCredentialsDetails,
INodeParameters,
ITaskData,
IConnections,
INodeTypeNameVersion,
IConnection,
IPinData,
} from 'n8n-workflow';
import type {
@@ -43,12 +53,15 @@ import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useCredentialsStore } from '@/stores/credentials.store';
import { get } from 'lodash-es';
import { useI18n } from './useI18n';
import { EnableNodeToggleCommand } from '@/models/history';
import { AddNodeCommand, EnableNodeToggleCommand, RemoveConnectionCommand } from '@/models/history';
import { useTelemetry } from './useTelemetry';
import { hasPermission } from '@/utils/rbac/permissions';
import type { N8nPlusEndpoint } from '@/plugins/jsplumb/N8nPlusEndpointType';
import * as NodeViewUtils from '@/utils/nodeViewUtils';
import { useCanvasStore } from '@/stores/canvas.store';
import { getEndpointScope } from '@/utils/nodeViewUtils';
import { useSourceControlStore } from '@/stores/sourceControl.store';
import { getConnectionInfo } from '@/utils/canvasUtils';
declare namespace HttpRequestNode {
namespace V2 {
@@ -67,6 +80,13 @@ export function useNodeHelpers() {
const workflowsStore = useWorkflowsStore();
const i18n = useI18n();
const canvasStore = useCanvasStore();
const sourceControlStore = useSourceControlStore();
const route = useRoute();
const isInsertingNodes = ref(false);
const credentialsUpdated = ref(false);
const isProductionExecutionPreview = ref(false);
const pullConnActiveNodeName = ref<string | null>(null);
function hasProxyAuth(node: INodeUi): boolean {
return Object.keys(node.parameters).includes('nodeCredentialType');
@@ -776,6 +796,430 @@ export function useNodeHelpers() {
});
}
function matchCredentials(node: INodeUi) {
if (!node.credentials) {
return;
}
Object.entries(node.credentials).forEach(
([nodeCredentialType, nodeCredentials]: [string, INodeCredentialsDetails]) => {
const credentialOptions = credentialsStore.getCredentialsByType(nodeCredentialType);
// Check if workflows applies old credentials style
if (typeof nodeCredentials === 'string') {
nodeCredentials = {
id: null,
name: nodeCredentials,
};
credentialsUpdated.value = true;
}
if (nodeCredentials.id) {
// Check whether the id is matching with a credential
const credentialsId = nodeCredentials.id.toString(); // due to a fixed bug in the migration UpdateWorkflowCredentials (just sqlite) we have to cast to string and check later if it has been a number
const credentialsForId = credentialOptions.find(
(optionData: ICredentialsResponse) => optionData.id === credentialsId,
);
if (credentialsForId) {
if (
credentialsForId.name !== nodeCredentials.name ||
typeof nodeCredentials.id === 'number'
) {
node.credentials![nodeCredentialType] = {
id: credentialsForId.id,
name: credentialsForId.name,
};
credentialsUpdated.value = true;
}
return;
}
}
// No match for id found or old credentials type used
node.credentials![nodeCredentialType] = nodeCredentials;
// check if only one option with the name would exist
const credentialsForName = credentialOptions.filter(
(optionData: ICredentialsResponse) => optionData.name === nodeCredentials.name,
);
// only one option exists for the name, take it
if (credentialsForName.length === 1) {
node.credentials![nodeCredentialType].id = credentialsForName[0].id;
credentialsUpdated.value = true;
}
},
);
}
function deleteJSPlumbConnection(connection: Connection, trackHistory = false) {
// Make sure to remove the overlay else after the second move
// it visibly stays behind free floating without a connection.
connection.removeOverlays();
pullConnActiveNodeName.value = null; // prevent new connections when connectionDetached is triggered
canvasStore.jsPlumbInstance?.deleteConnection(connection); // on delete, triggers connectionDetached event which applies mutation to store
if (trackHistory && connection.__meta) {
const connectionData: [IConnection, IConnection] = [
{
index: connection.__meta?.sourceOutputIndex,
node: connection.__meta.sourceNodeName,
type: NodeConnectionType.Main,
},
{
index: connection.__meta?.targetOutputIndex,
node: connection.__meta.targetNodeName,
type: NodeConnectionType.Main,
},
];
const removeCommand = new RemoveConnectionCommand(connectionData);
historyStore.pushCommandToUndo(removeCommand);
}
}
async function loadNodesProperties(nodeInfos: INodeTypeNameVersion[]): Promise<void> {
const allNodes: INodeTypeDescription[] = nodeTypesStore.allNodeTypes;
const nodesToBeFetched: INodeTypeNameVersion[] = [];
allNodes.forEach((node) => {
const nodeVersions = Array.isArray(node.version) ? node.version : [node.version];
if (
!!nodeInfos.find((n) => n.name === node.name && nodeVersions.includes(n.version)) &&
!node.hasOwnProperty('properties')
) {
nodesToBeFetched.push({
name: node.name,
version: Array.isArray(node.version) ? node.version.slice(-1)[0] : node.version,
});
}
});
if (nodesToBeFetched.length > 0) {
// Only call API if node information is actually missing
canvasStore.startLoading();
await nodeTypesStore.getNodesInformation(nodesToBeFetched);
canvasStore.stopLoading();
}
}
function addConnectionsTestData() {
canvasStore.jsPlumbInstance?.connections.forEach((connection) => {
NodeViewUtils.addConnectionTestData(
connection.source,
connection.target,
connection?.connector?.hasOwnProperty('canvas') ? connection?.connector.canvas : undefined,
);
});
}
async function processConnectionBatch(batchedConnectionData: Array<[IConnection, IConnection]>) {
const batchSize = 100;
for (let i = 0; i < batchedConnectionData.length; i += batchSize) {
const batch = batchedConnectionData.slice(i, i + batchSize);
batch.forEach((connectionData) => {
addConnection(connectionData);
});
}
}
function addPinDataConnections(pinData?: IPinData) {
if (!pinData) {
return;
}
Object.keys(pinData).forEach((nodeName) => {
const node = workflowsStore.getNodeByName(nodeName);
if (!node) {
return;
}
const nodeElement = document.getElementById(node.id);
if (!nodeElement) {
return;
}
const hasRun = workflowsStore.getWorkflowResultDataByNodeName(nodeName) !== null;
// In case we are showing a production execution preview we want
// to show pinned data connections as they wouldn't have been pinned
const classNames = isProductionExecutionPreview.value ? [] : ['pinned'];
if (hasRun) {
classNames.push('has-run');
}
const connections = canvasStore.jsPlumbInstance?.getConnections({
source: nodeElement,
});
const connectionsArray = Array.isArray(connections)
? connections
: Object.values(connections);
connectionsArray.forEach((connection) => {
NodeViewUtils.addConnectionOutputSuccess(connection, {
total: pinData[nodeName].length,
iterations: 0,
classNames,
});
});
});
}
function removePinDataConnections(pinData: IPinData) {
Object.keys(pinData).forEach((nodeName) => {
const node = workflowsStore.getNodeByName(nodeName);
if (!node) {
return;
}
const nodeElement = document.getElementById(node.id);
if (!nodeElement) {
return;
}
const connections = canvasStore.jsPlumbInstance?.getConnections({
source: nodeElement,
});
const connectionsArray = Array.isArray(connections)
? connections
: Object.values(connections);
canvasStore.jsPlumbInstance.setSuspendDrawing(true);
connectionsArray.forEach(NodeViewUtils.resetConnection);
canvasStore.jsPlumbInstance.setSuspendDrawing(false, true);
});
}
function getOutputEndpointUUID(
nodeName: string,
connectionType: NodeConnectionType,
index: number,
): string | null {
const node = workflowsStore.getNodeByName(nodeName);
if (!node) {
return null;
}
return NodeViewUtils.getOutputEndpointUUID(node.id, connectionType, index);
}
function getInputEndpointUUID(
nodeName: string,
connectionType: NodeConnectionType,
index: number,
) {
const node = workflowsStore.getNodeByName(nodeName);
if (!node) {
return null;
}
return NodeViewUtils.getInputEndpointUUID(node.id, connectionType, index);
}
function addConnection(connection: [IConnection, IConnection]) {
const outputUuid = getOutputEndpointUUID(
connection[0].node,
connection[0].type,
connection[0].index,
);
const inputUuid = getInputEndpointUUID(
connection[1].node,
connection[1].type,
connection[1].index,
);
if (!outputUuid || !inputUuid) {
return;
}
const uuids: [string, string] = [outputUuid, inputUuid];
// Create connections in DOM
canvasStore.jsPlumbInstance?.connect({
uuids,
detachable: !route?.meta?.readOnlyCanvas && !sourceControlStore.preferences.branchReadOnly,
});
setTimeout(() => {
addPinDataConnections(workflowsStore.pinnedWorkflowData);
});
}
function removeConnection(
connection: [IConnection, IConnection],
removeVisualConnection = false,
) {
if (removeVisualConnection) {
const sourceNode = workflowsStore.getNodeByName(connection[0].node);
const targetNode = workflowsStore.getNodeByName(connection[1].node);
if (!sourceNode || !targetNode) {
return;
}
const sourceElement = document.getElementById(sourceNode.id);
const targetElement = document.getElementById(targetNode.id);
if (sourceElement && targetElement) {
const connections = canvasStore.jsPlumbInstance?.getConnections({
source: sourceElement,
target: targetElement,
});
if (Array.isArray(connections)) {
connections.forEach((connectionInstance: Connection) => {
if (connectionInstance.__meta) {
// Only delete connections from specific indexes (if it can be determined by meta)
if (
connectionInstance.__meta.sourceOutputIndex === connection[0].index &&
connectionInstance.__meta.targetOutputIndex === connection[1].index
) {
deleteJSPlumbConnection(connectionInstance);
}
} else {
deleteJSPlumbConnection(connectionInstance);
}
});
}
}
}
workflowsStore.removeConnection({ connection });
}
function removeConnectionByConnectionInfo(
info: ConnectionDetachedParams,
removeVisualConnection = false,
trackHistory = false,
) {
const connectionInfo: [IConnection, IConnection] | null = getConnectionInfo(info);
if (connectionInfo) {
if (removeVisualConnection) {
deleteJSPlumbConnection(info.connection, trackHistory);
} else if (trackHistory) {
historyStore.pushCommandToUndo(new RemoveConnectionCommand(connectionInfo));
}
workflowsStore.removeConnection({ connection: connectionInfo });
}
}
async function addConnections(connections: IConnections) {
const batchedConnectionData: Array<[IConnection, IConnection]> = [];
for (const sourceNode in connections) {
for (const type in connections[sourceNode]) {
connections[sourceNode][type].forEach((outwardConnections, sourceIndex) => {
if (outwardConnections) {
outwardConnections.forEach((targetData) => {
batchedConnectionData.push([
{
node: sourceNode,
type: getEndpointScope(type) ?? NodeConnectionType.Main,
index: sourceIndex,
},
{ node: targetData.node, type: targetData.type, index: targetData.index },
]);
});
}
});
}
}
// Process the connections in batches
await processConnectionBatch(batchedConnectionData);
setTimeout(addConnectionsTestData, 0);
}
async function addNodes(nodes: INodeUi[], connections?: IConnections, trackHistory = false) {
if (!nodes?.length) {
return;
}
isInsertingNodes.value = true;
// Before proceeding we must check if all nodes contain the `properties` attribute.
// Nodes are loaded without this information so we must make sure that all nodes
// being added have this information.
await loadNodesProperties(
nodes.map((node) => ({ name: node.type, version: node.typeVersion })),
);
// Add the node to the node-list
let nodeType: INodeTypeDescription | null;
nodes.forEach((node) => {
const newNode: INodeUi = {
...node,
};
if (!newNode.id) {
newNode.id = uuid();
}
nodeType = nodeTypesStore.getNodeType(newNode.type, newNode.typeVersion);
// Make sure that some properties always exist
if (!newNode.hasOwnProperty('disabled')) {
newNode.disabled = false;
}
if (!newNode.hasOwnProperty('parameters')) {
newNode.parameters = {};
}
// Load the default parameter values because only values which differ
// from the defaults get saved
if (nodeType !== null) {
let nodeParameters = null;
try {
nodeParameters = NodeHelpers.getNodeParameters(
nodeType.properties,
newNode.parameters,
true,
false,
node,
);
} catch (e) {
console.error(
i18n.baseText('nodeView.thereWasAProblemLoadingTheNodeParametersOfNode') +
`: "${newNode.name}"`,
);
console.error(e);
}
newNode.parameters = nodeParameters ?? {};
// if it's a webhook and the path is empty set the UUID as the default path
if (
[WEBHOOK_NODE_TYPE, FORM_TRIGGER_NODE_TYPE].includes(newNode.type) &&
newNode.parameters.path === ''
) {
newNode.parameters.path = newNode.webhookId as string;
}
}
// check and match credentials, apply new format if old is used
matchCredentials(newNode);
workflowsStore.addNode(newNode);
if (trackHistory) {
historyStore.pushCommandToUndo(new AddNodeCommand(newNode));
}
});
// Wait for the nodes to be rendered
await nextTick();
canvasStore.jsPlumbInstance?.setSuspendDrawing(true);
if (connections) {
await addConnections(connections);
}
// Add the node issues at the end as the node-connections are required
refreshNodeIssues();
updateNodesInputIssues();
/////////////////////////////this.resetEndpointsErrors();
isInsertingNodes.value = false;
// Now it can draw again
canvasStore.jsPlumbInstance?.setSuspendDrawing(false, true);
}
return {
hasProxyAuth,
isCustomApiCallSelected,
@@ -795,5 +1239,17 @@ export function useNodeHelpers() {
updateNodesCredentialsIssues,
getNodeInputData,
setSuccessOutput,
isInsertingNodes,
credentialsUpdated,
isProductionExecutionPreview,
pullConnActiveNodeName,
deleteJSPlumbConnection,
loadNodesProperties,
addNodes,
addConnection,
removeConnection,
removeConnectionByConnectionInfo,
addPinDataConnections,
removePinDataConnections,
};
}