fix(editor): Color node connections correctly in execution preview for nodes that have pinned data (#9669)

This commit is contained in:
Tomi Turtiainen 2024-06-07 16:12:59 +03:00 committed by GitHub
parent 6ae6a5ebdf
commit ebba7c87cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 170 additions and 27 deletions

View file

@ -84,7 +84,8 @@ describe('Current Workflow Executions', () => {
executionsTab.actions.switchToExecutionsTab();
cy.wait(['@getExecution']);
cy.getByTestId('workflow-preview-iframe')
executionsTab.getters
.workflowExecutionPreviewIframe()
.should('be.visible')
.its('0.contentDocument.body') // Access the body of the iframe document
.should('not.be.empty') // Ensure the body is not empty

View file

@ -0,0 +1,65 @@
import { v4 as uuid } from 'uuid';
import { WorkflowExecutionsTab, WorkflowPage as WorkflowPageClass } from '../pages';
import { BACKEND_BASE_URL } from '../constants';
const workflowPage = new WorkflowPageClass();
const executionsTab = new WorkflowExecutionsTab();
describe('ADO-2106 connections should be colored correctly for pinned data in executions preview', () => {
beforeEach(() => {
workflowPage.actions.visit();
});
beforeEach(() => {
cy.createFixtureWorkflow('Webhook_set_pinned.json', `Webhook set pinned ${uuid()}`);
workflowPage.actions.deselectAll();
workflowPage.getters.zoomToFitButton().click();
workflowPage.getters.getConnectionBetweenNodes('Webhook', 'Set').should('have.class', 'pinned');
});
it('should not color connections for pinned data nodes for production executions', () => {
workflowPage.actions.activateWorkflow();
// Execute the workflow
cy.request('POST', `${BACKEND_BASE_URL}/webhook/23fc3930-b8f9-41d9-89db-b647291a2201`, {
here: 'is some data',
}).then((response) => {
expect(response.status).to.eq(200);
});
executionsTab.actions.switchToExecutionsTab();
executionsTab.getters.successfulExecutionListItems().should('have.length', 1);
executionsTab.getters
.workflowExecutionPreviewIframe()
.should('be.visible')
.its('0.contentDocument.body')
.should('not.be.empty')
.then(cy.wrap)
.find(`.jtk-connector[data-source-node="Webhook"][data-target-node="Set"]`)
.should('have.class', 'success')
.should('have.class', 'has-run')
.should('not.have.class', 'pinned');
});
it('should color connections for pinned data nodes for manual executions', () => {
workflowPage.actions.executeWorkflow();
executionsTab.actions.switchToExecutionsTab();
executionsTab.getters.successfulExecutionListItems().should('have.length', 1);
executionsTab.getters
.workflowExecutionPreviewIframe()
.should('be.visible')
.its('0.contentDocument.body')
.should('not.be.empty')
.then(cy.wrap)
.find(`.jtk-connector[data-source-node="Webhook"][data-target-node="Set"]`)
.should('have.class', 'success')
.should('have.class', 'has-run')
.should('have.class', 'pinned');
});
});

View file

@ -0,0 +1,67 @@
{
"nodes": [
{
"parameters": {
"options": {}
},
"id": "bd816131-d8ad-4b4c-90d6-59fdab2e6307",
"name": "Set",
"type": "n8n-nodes-base.set",
"typeVersion": 1,
"position": [
720,
460
]
},
{
"parameters": {
"httpMethod": "POST",
"path": "23fc3930-b8f9-41d9-89db-b647291a2201",
"options": {}
},
"id": "82fe0f6c-854a-4eb9-b311-d7b43025c047",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [
460,
460
],
"webhookId": "23fc3930-b8f9-41d9-89db-b647291a2201"
}
],
"connections": {
"Webhook": {
"main": [
[
{
"node": "Set",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Webhook": [
{
"headers": {
"host": "localhost:5678",
"content-length": "37",
"accept": "*/*",
"content-type": "application/json",
"accept-encoding": "gzip"
},
"params": {},
"query": {},
"body": {
"here": "be",
"dragons": true
},
"webhookUrl": "http://localhost:5678/webhook-test/23fc3930-b8f9-41d9-89db-b647291a2201",
"executionMode": "test"
}
]
}
}

View file

@ -24,6 +24,7 @@ export class WorkflowExecutionsTab extends BasePage {
executionPreviewId: () =>
this.getters.executionPreviewDetails().find('[data-test-id="execution-preview-id"]'),
executionDebugButton: () => cy.getByTestId('execution-debug-button'),
workflowExecutionPreviewIframe: () => cy.getByTestId('workflow-preview-iframe'),
};
actions = {
toggleNodeEnabled: (nodeName: string) => {

View file

@ -108,9 +108,9 @@
</div>
<div
v-if="showDisabledLinethrough"
v-if="showDisabledLineThrough"
:class="{
'disabled-linethrough': true,
'disabled-line-through': true,
success: !['unknown'].includes(nodeExecutionStatus) && workflowDataItems > 0,
}"
></div>
@ -187,7 +187,7 @@ import {
LOCAL_STORAGE_PIN_DATA_DISCOVERY_CANVAS_FLAG,
MANUAL_TRIGGER_NODE_TYPE,
NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS,
NOT_DUPLICATABE_NODE_TYPES,
NOT_DUPLICATABLE_NODE_TYPES,
SIMULATE_NODE_TYPE,
SIMULATE_TRIGGER_NODE_TYPE,
WAIT_TIME_UNLIMITED,
@ -287,7 +287,7 @@ export default defineComponent({
},
isDuplicatable(): boolean {
if (!this.nodeType) return true;
if (NOT_DUPLICATABE_NODE_TYPES.includes(this.nodeType.name)) return false;
if (NOT_DUPLICATABLE_NODE_TYPES.includes(this.nodeType.name)) return false;
return (
this.nodeType.maxNodes === undefined || this.sameTypeNodes.length < this.nodeType.maxNodes
);
@ -493,7 +493,7 @@ export default defineComponent({
position(): XYPosition {
return this.node ? this.node.position : [0, 0];
},
showDisabledLinethrough(): boolean {
showDisabledLineThrough(): boolean {
return (
!this.isConfigurableNode &&
!!(this.data?.disabled && this.inputs.length === 1 && this.outputs.length === 1)
@ -1118,7 +1118,7 @@ export default defineComponent({
}
}
.disabled-linethrough {
.disabled-line-through {
border: 1px solid var(--color-foreground-dark);
position: absolute;
top: 49px;
@ -1189,7 +1189,7 @@ export default defineComponent({
overflow: auto;
}
.disabled-linethrough {
.disabled-line-through {
z-index: 8;
}

View file

@ -27,7 +27,6 @@ import { onMounted, onBeforeUnmount, ref, computed, watch } from 'vue';
import { useI18n } from '@/composables/useI18n';
import { useToast } from '@/composables/useToast';
import type { IWorkflowDb, IWorkflowTemplate } from '@/Interface';
import { useRootStore } from '@/stores/n8nRoot.store';
import { useExecutionsStore } from '@/stores/executions.store';
const props = withDefaults(
@ -44,6 +43,9 @@ const props = withDefaults(
{
loading: false,
mode: 'workflow',
workflow: undefined,
executionId: undefined,
executionMode: undefined,
loaderType: 'image',
canOpenNDV: true,
hideNodeIssues: false,
@ -56,7 +58,6 @@ const emit = defineEmits<{
const i18n = useI18n();
const toast = useToast();
const rootStore = useRootStore();
const executionsStore = useExecutionsStore();
const iframeRef = ref<HTMLIFrameElement | null>(null);
@ -73,8 +74,8 @@ const iframeSrc = computed(() => {
const showPreview = computed(() => {
return (
!props.loading &&
((props.mode === 'workflow' && props.workflow) ||
(props.mode === 'execution' && props.executionId)) &&
((props.mode === 'workflow' && !!props.workflow) ||
(props.mode === 'execution' && !!props.executionId)) &&
ready.value
);
});
@ -114,7 +115,7 @@ const loadExecution = () => {
JSON.stringify({
command: 'openExecution',
executionId: props.executionId,
executionMode: props.executionMode || '',
executionMode: props.executionMode ?? '',
canOpenNDV: props.canOpenNDV,
}),
'*',

View file

@ -1,5 +1,5 @@
import type { ActionDropdownItem, XYPosition } from '@/Interface';
import { NOT_DUPLICATABE_NODE_TYPES, STICKY_NODE_TYPE } from '@/constants';
import { NOT_DUPLICATABLE_NODE_TYPES, STICKY_NODE_TYPE } from '@/constants';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useSourceControlStore } from '@/stores/sourceControl.store';
import { useUIStore } from '@/stores/ui.store';
@ -72,7 +72,7 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) =
const canDuplicateNode = (node: INode): boolean => {
const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion);
if (!nodeType) return false;
if (NOT_DUPLICATABE_NODE_TYPES.includes(nodeType.name)) return false;
if (NOT_DUPLICATABLE_NODE_TYPES.includes(nodeType.name)) return false;
return canAddNodeOfType(nodeType);
};

View file

@ -752,7 +752,7 @@ export const APPEND_ATTRIBUTION_DEFAULT_PATH = 'parameters.options.appendAttribu
export const DRAG_EVENT_DATA_KEY = 'nodesAndConnections';
export const NOT_DUPLICATABE_NODE_TYPES = [FORM_TRIGGER_NODE_TYPE];
export const NOT_DUPLICATABLE_NODE_TYPES = [FORM_TRIGGER_NODE_TYPE];
export const UPDATE_WEBHOOK_ID_NODE_TYPES = [FORM_TRIGGER_NODE_TYPE];
export const CREATOR_HUB_URL = 'https://creators.n8n.io/hub';

View file

@ -915,7 +915,7 @@ export default defineComponent({
setTimeout(() => {
void this.usersStore.showPersonalizationSurvey();
this.addPinDataConnections(this.workflowsStore.pinnedWorkflowData || ({} as IPinData));
this.addPinDataConnections(this.workflowsStore.pinnedWorkflowData);
}, 0);
});
@ -2332,7 +2332,7 @@ export default defineComponent({
this.workflowsStore.addWorkflowTagIds(tagIds);
setTimeout(() => {
this.addPinDataConnections(this.workflowsStore.pinnedWorkflowData || ({} as IPinData));
this.addPinDataConnections(this.workflowsStore.pinnedWorkflowData);
});
}
} catch (error) {
@ -3892,7 +3892,7 @@ export default defineComponent({
});
setTimeout(() => {
this.addPinDataConnections(this.workflowsStore.pinnedWorkflowData ?? ({} as IPinData));
this.addPinDataConnections(this.workflowsStore.pinnedWorkflowData);
});
},
__removeConnection(connection: [IConnection, IConnection], removeVisualConnection = false) {
@ -4904,23 +4904,31 @@ export default defineComponent({
await this.importWorkflowData(workflowData, 'url');
}
},
addPinDataConnections(pinData: IPinData) {
addPinDataConnections(pinData?: IPinData) {
if (!pinData) {
return;
}
Object.keys(pinData).forEach((nodeName) => {
const node = this.workflowsStore.getNodeByName(nodeName);
if (!node) {
return;
}
const hasRun = this.workflowsStore.getWorkflowResultDataByNodeName(nodeName) !== null;
const classNames = ['pinned'];
if (hasRun) {
classNames.push('has-run');
}
const nodeElement = document.getElementById(node.id);
if (!nodeElement) {
return;
}
const hasRun = this.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 = this.isProductionExecutionPreview ? [] : ['pinned'];
if (hasRun) {
classNames.push('has-run');
}
const connections = this.instance?.getConnections({
source: nodeElement,
});
@ -5055,7 +5063,7 @@ export default defineComponent({
});
}
this.addPinDataConnections(this.workflowsStore.pinnedWorkflowData || ({} as IPinData));
this.addPinDataConnections(this.workflowsStore.pinnedWorkflowData);
},
async saveCurrentWorkflowExternal(callback: () => void) {