diff --git a/cypress/e2e/19-execution.cy.ts b/cypress/e2e/19-execution.cy.ts index 93e172cc55..e8e4990e43 100644 --- a/cypress/e2e/19-execution.cy.ts +++ b/cypress/e2e/19-execution.cy.ts @@ -51,6 +51,10 @@ describe('Execution', () => { .canvasNodeByName('Manual') .within(() => cy.get('.fa-check')) .should('exist'); + workflowPage.getters + .canvasNodeByName('Wait') + .within(() => cy.get('.fa-check')) + .should('exist'); workflowPage.getters .canvasNodeByName('Set') .within(() => cy.get('.fa-check')) @@ -120,8 +124,8 @@ describe('Execution', () => { workflowPage.getters.clearExecutionDataButton().click(); workflowPage.getters.clearExecutionDataButton().should('not.exist'); - // Check warning toast (works because Cypress waits enough for the element to show after the http request node has finished) - workflowPage.getters.warningToast().should('be.visible'); + // Check success toast (works because Cypress waits enough for the element to show after the http request node has finished) + workflowPage.getters.successToast().should('be.visible'); }); it('should test webhook workflow', () => { @@ -267,7 +271,7 @@ describe('Execution', () => { workflowPage.getters.clearExecutionDataButton().click(); workflowPage.getters.clearExecutionDataButton().should('not.exist'); - // Check warning toast (works because Cypress waits enough for the element to show after the http request node has finished) - workflowPage.getters.warningToast().should('be.visible'); + // Check success toast (works because Cypress waits enough for the element to show after the http request node has finished) + workflowPage.getters.successToast().should('be.visible'); }); }); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 4cd7b924a2..6a0f122464 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -1,6 +1,6 @@ import 'cypress-real-events'; import { WorkflowPage } from '../pages'; -import { BACKEND_BASE_URL, N8N_AUTH_COOKIE } from '../constants'; +import { BACKEND_BASE_URL, INSTANCE_MEMBERS, INSTANCE_OWNER, N8N_AUTH_COOKIE } from '../constants'; Cypress.Commands.add('getByTestId', (selector, ...args) => { return cy.get(`[data-test-id="${selector}"]`, ...args); @@ -169,6 +169,13 @@ Cypress.Commands.add('draganddrop', (draggableSelector, droppableSelector) => { }); }); +Cypress.Commands.add('push', (type, data) => { + cy.request('POST', `${BACKEND_BASE_URL}/rest/e2e/push`, { + type, + data, + }); +}); + Cypress.Commands.add('shouldNotHaveConsoleErrors', () => { cy.window().then((win) => { const spy = cy.spy(win.console, 'error'); diff --git a/cypress/support/index.ts b/cypress/support/index.ts index 77180a7ce5..13bd1ff0ef 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -39,6 +39,7 @@ declare global { options?: { abs?: boolean; index?: number; realMouse?: boolean; clickToFinish?: boolean }, ): void; draganddrop(draggableSelector: string, droppableSelector: string): void; + push(type: string, data: unknown): void; shouldNotHaveConsoleErrors(): void; } } diff --git a/package.json b/package.json index de8d622c93..9348b49f1a 100644 --- a/package.json +++ b/package.json @@ -47,8 +47,8 @@ "@types/supertest": "^2.0.12", "@vitest/coverage-v8": "^0.33.0", "cross-env": "^7.0.3", - "cypress-otp": "^1.0.3", "cypress": "^12.17.2", + "cypress-otp": "^1.0.3", "cypress-real-events": "^1.9.1", "jest": "^29.6.2", "jest-environment-jsdom": "^29.6.2", diff --git a/packages/cli/src/controllers/e2e.controller.ts b/packages/cli/src/controllers/e2e.controller.ts index 5b1184229f..8a41b81207 100644 --- a/packages/cli/src/controllers/e2e.controller.ts +++ b/packages/cli/src/controllers/e2e.controller.ts @@ -1,5 +1,5 @@ import { Request } from 'express'; -import { Service } from 'typedi'; +import { Container, Service } from 'typedi'; import { v4 as uuid } from 'uuid'; import config from '@/config'; import type { Role } from '@db/entities/Role'; @@ -13,8 +13,9 @@ import { License } from '@/License'; import { LICENSE_FEATURES, inE2ETests } from '@/constants'; import { NoAuthRequired, Patch, Post, RestController } from '@/decorators'; import type { UserSetupPayload } from '@/requests'; -import type { BooleanLicenseFeature } from '@/Interfaces'; +import type { BooleanLicenseFeature, IPushDataType } from '@/Interfaces'; import { MfaService } from '@/Mfa/mfa.service'; +import { Push } from '@/push'; if (!inE2ETests) { console.error('E2E endpoints only allowed during E2E tests'); @@ -51,6 +52,16 @@ type ResetRequest = Request< } >; +type PushRequest = Request< + {}, + {}, + { + type: IPushDataType; + sessionId: string; + data: object; + } +>; + @Service() @NoAuthRequired() @RestController('/e2e') @@ -95,6 +106,17 @@ export class E2EController { await this.setupUserManagement(req.body.owner, req.body.members); } + @Post('/push') + async push(req: PushRequest) { + const pushInstance = Container.get(Push); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const sessionId = Object.keys(pushInstance.getBackend().connections as object)[0]; + + pushInstance.send(req.body.type, req.body.data, sessionId); + } + @Patch('/feature') setFeature(req: Request<{}, {}, { feature: BooleanLicenseFeature; enabled: boolean }>) { const { enabled, feature } = req.body; diff --git a/packages/cli/src/push/index.ts b/packages/cli/src/push/index.ts index 0cbd28da3c..e89c5a7a5b 100644 --- a/packages/cli/src/push/index.ts +++ b/packages/cli/src/push/index.ts @@ -63,6 +63,10 @@ export class Push extends EventEmitter { this.backend.send(type, data, sessionId); } + getBackend() { + return this.backend; + } + sendToUsers(type: IPushDataType, data: D, userIds: Array) { this.backend.sendToUsers(type, data, userIds); } diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 18bf12dbd8..fbd5769eaf 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -3301,9 +3301,11 @@ export function getExecuteFunctions( // Display on the calling node which node has the error throw new NodeOperationError( connectedNode, - `Error on node "${connectedNode.name}" which is connected via input "${inputName}"`, + `Error in sub-node ${connectedNode.name}`, { itemIndex, + functionality: 'configuration-node', + description: error.message, }, ); } diff --git a/packages/design-system/src/components/N8nNodeIcon/NodeIcon.vue b/packages/design-system/src/components/N8nNodeIcon/NodeIcon.vue index cc355677fa..399b46a4e7 100644 --- a/packages/design-system/src/components/N8nNodeIcon/NodeIcon.vue +++ b/packages/design-system/src/components/N8nNodeIcon/NodeIcon.vue @@ -9,7 +9,7 @@ :style="iconStyleData" > - +
@@ -78,6 +78,10 @@ export default defineComponent({ showTooltip: { type: Boolean, }, + tooltipPosition: { + type: String, + default: 'top', + }, badge: { type: Object as PropType<{ src: string; type: string }> }, }, computed: { diff --git a/packages/design-system/src/components/N8nNotice/Notice.vue b/packages/design-system/src/components/N8nNotice/Notice.vue index 93a34cc810..271bb26b14 100644 --- a/packages/design-system/src/components/N8nNotice/Notice.vue +++ b/packages/design-system/src/components/N8nNotice/Notice.vue @@ -66,7 +66,9 @@ export default defineComponent({ }, sanitizeHtml(text: string): string { return sanitizeHtml(text, { - allowedAttributes: { a: ['data-key', 'href', 'target'] }, + allowedAttributes: { + a: ['data-key', 'href', 'target', 'data-action', 'data-action-parameter-connectiontype'], + }, }); }, onClick(event: MouseEvent) { diff --git a/packages/design-system/src/css/_tokens.dark.scss b/packages/design-system/src/css/_tokens.dark.scss index ca3c792062..83705187de 100644 --- a/packages/design-system/src/css/_tokens.dark.scss +++ b/packages/design-system/src/css/_tokens.dark.scss @@ -31,6 +31,13 @@ // Secondary tokens + // LangChain + --color-lm-chat-messages-background: var(--prim-gray-820); + --color-lm-chat-bot-background: var(--prim-gray-540); + --color-lm-chat-bot-border: var(--prim-gray-490); + --color-lm-chat-user-background: var(--prim-color-alt-a-shade-100); + --color-lm-chat-user-border: var(--prim-color-alt-a); + // Canvas --color-canvas-background: var(--prim-gray-820); --color-canvas-dot: var(--prim-gray-670); @@ -174,6 +181,11 @@ --border-color-light: var(--color-foreground-light); --border-base: var(--border-width-base) var(--border-style-base) var(--color-foreground-base); --node-type-supplemental-label-color-l: 100%; + --node-type-supplemental-label-color: hsl( + var(--node-type-supplemental-label-color-h), + var(--node-type-supplemental-label-color-s), + var(--node-type-supplemental-label-color-l) + ); --color-configurable-node-name: var(--color-text-lighter); --color-secondary-link: var(--prim-color-secondary-tint-200); --color-secondary-link-hover: var(--prim-color-secondary-tint-100); diff --git a/packages/design-system/src/css/_tokens.scss b/packages/design-system/src/css/_tokens.scss index ee6c7d33a3..b87ab54c86 100644 --- a/packages/design-system/src/css/_tokens.scss +++ b/packages/design-system/src/css/_tokens.scss @@ -64,6 +64,13 @@ // Secondary tokens + // LangChain + --color-lm-chat-messages-background: var(--color-background-base); + --color-lm-chat-bot-background: var(--prim-gray-120); + --color-lm-chat-bot-border: var(--prim-gray-200); + --color-lm-chat-user-background: var(--prim-color-alt-a-tint-400); + --color-lm-chat-user-border: var(--prim-color-alt-a-tint-300); + // Canvas --color-canvas-background: var(--prim-gray-10); --color-canvas-dot: var(--prim-gray-120); diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index 45e47d8009..09258aef9b 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -87,6 +87,7 @@ "@faker-js/faker": "^8.0.2", "@pinia/testing": "^0.1.3", "@sentry/vite-plugin": "^2.5.0", + "@testing-library/vue": "^7.0.0", "@types/dateformat": "^3.0.0", "@types/file-saver": "^2.0.1", "@types/humanize-duration": "^3.27.1", diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 96a49f53b7..e54a4b54d4 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -1269,6 +1269,7 @@ export type NodeCreatorOpenSource = | 'tab' | 'node_connection_action' | 'node_connection_drop' + | 'notice_error_message' | 'add_node_button'; export interface INodeCreatorState { diff --git a/packages/editor-ui/src/__tests__/defaults.ts b/packages/editor-ui/src/__tests__/defaults.ts new file mode 100644 index 0000000000..e6ef46d09f --- /dev/null +++ b/packages/editor-ui/src/__tests__/defaults.ts @@ -0,0 +1,47 @@ +import type { INodeTypeData, INodeTypeDescription } from 'n8n-workflow'; +import { + AGENT_NODE_TYPE, + MANUAL_CHAT_TRIGGER_NODE_TYPE, + MANUAL_TRIGGER_NODE_TYPE, +} from '@/constants'; +import nodeTypesJson from '../../../nodes-base/dist/types/nodes.json'; + +const allNodeTypes = [...nodeTypesJson]; + +function findNodeWithName(name: string): INodeTypeDescription { + return allNodeTypes.find((node) => node.name === name) as INodeTypeDescription; +} + +export const testingNodeTypes: INodeTypeData = { + [MANUAL_TRIGGER_NODE_TYPE]: { + sourcePath: '', + type: { + description: findNodeWithName(MANUAL_TRIGGER_NODE_TYPE), + }, + }, + [MANUAL_CHAT_TRIGGER_NODE_TYPE]: { + sourcePath: '', + type: { + description: findNodeWithName(MANUAL_CHAT_TRIGGER_NODE_TYPE), + }, + }, + [AGENT_NODE_TYPE]: { + sourcePath: '', + type: { + description: findNodeWithName(AGENT_NODE_TYPE), + }, + }, +}; + +export const defaultMockNodeTypes: INodeTypeData = { + [MANUAL_TRIGGER_NODE_TYPE]: testingNodeTypes[MANUAL_TRIGGER_NODE_TYPE], +}; + +export function mockNodeTypesToArray(nodeTypes: INodeTypeData): INodeTypeDescription[] { + return Object.values(nodeTypes).map( + (nodeType) => nodeType.type.description as INodeTypeDescription, + ); +} + +export const defaultMockNodeTypesArray: INodeTypeDescription[] = + mockNodeTypesToArray(defaultMockNodeTypes); diff --git a/packages/editor-ui/src/__tests__/mocks.ts b/packages/editor-ui/src/__tests__/mocks.ts new file mode 100644 index 0000000000..3c1bc84a22 --- /dev/null +++ b/packages/editor-ui/src/__tests__/mocks.ts @@ -0,0 +1,111 @@ +import type { + INodeType, + INodeTypeData, + INodeTypes, + IVersionedNodeType, + IConnections, + IDataObject, + INode, + IPinData, + IWorkflowSettings, +} from 'n8n-workflow'; +import { NodeHelpers, Workflow } from 'n8n-workflow'; +import { uuid } from '@jsplumb/util'; +import { defaultMockNodeTypes } from '@/__tests__/defaults'; +import type { + INodeUi, + ITag, + IUsedCredential, + IUser, + IWorkflowDb, + WorkflowMetadata, +} from '@/Interface'; + +export function createTestNodeTypes(data: INodeTypeData = {}): INodeTypes { + const getResolvedKey = (key: string) => { + const resolvedKeyParts = key.split(/[\/.]/); + return resolvedKeyParts[resolvedKeyParts.length - 1]; + }; + + const nodeTypes = { + ...defaultMockNodeTypes, + ...Object.keys(data).reduce((acc, key) => { + acc[getResolvedKey(key)] = data[key]; + + return acc; + }, {}), + }; + + function getByName(nodeType: string): INodeType | IVersionedNodeType { + return nodeTypes[getResolvedKey(nodeType)].type; + } + + function getByNameAndVersion(nodeType: string, version?: number): INodeType { + return NodeHelpers.getVersionedNodeType(getByName(nodeType), version); + } + + return { + getByName, + getByNameAndVersion, + }; +} + +export function createTestWorkflowObject(options: { + id?: string; + name?: string; + nodes: INode[]; + connections: IConnections; + active?: boolean; + nodeTypes?: INodeTypeData; + staticData?: IDataObject; + settings?: IWorkflowSettings; + pinData?: IPinData; +}) { + return new Workflow({ + ...options, + id: options.id ?? uuid(), + active: options.active ?? false, + nodeTypes: createTestNodeTypes(options.nodeTypes), + connections: options.connections ?? {}, + }); +} + +export function createTestWorkflow(options: { + id?: string; + name: string; + active?: boolean; + createdAt?: number | string; + updatedAt?: number | string; + nodes?: INodeUi[]; + connections?: IConnections; + settings?: IWorkflowSettings; + tags?: ITag[] | string[]; + pinData?: IPinData; + sharedWith?: Array>; + ownedBy?: Partial; + versionId?: string; + usedCredentials?: IUsedCredential[]; + meta?: WorkflowMetadata; +}): IWorkflowDb { + return { + ...options, + createdAt: options.createdAt ?? '', + updatedAt: options.updatedAt ?? '', + versionId: options.versionId ?? '', + id: options.id ?? uuid(), + active: options.active ?? false, + connections: options.connections ?? {}, + } as IWorkflowDb; +} + +export function createTestNode( + node: Partial & { name: INode['name']; type: INode['type'] }, +): INode { + return { + id: uuid(), + typeVersion: 1, + position: [0, 0] as [number, number], + parameters: {}, + ...node, + }; +} diff --git a/packages/editor-ui/src/__tests__/server/endpoints/settings.ts b/packages/editor-ui/src/__tests__/server/endpoints/settings.ts index 14e8c3a620..9dacbb8c37 100644 --- a/packages/editor-ui/src/__tests__/server/endpoints/settings.ts +++ b/packages/editor-ui/src/__tests__/server/endpoints/settings.ts @@ -41,6 +41,7 @@ const defaultSettings: IN8nUISettings = { oauthCallbackUrls: { oauth1: '', oauth2: '' }, onboardingCallPromptEnabled: false, personalizationSurveyEnabled: false, + releaseChannel: 'stable', posthog: { apiHost: '', apiKey: '', diff --git a/packages/editor-ui/src/components/ChatEmbedModal.vue b/packages/editor-ui/src/components/ChatEmbedModal.vue index fe979122c7..f4538f9cdd 100644 --- a/packages/editor-ui/src/components/ChatEmbedModal.vue +++ b/packages/editor-ui/src/components/ChatEmbedModal.vue @@ -5,7 +5,6 @@ import type { EventBus } from 'n8n-design-system/utils'; import { createEventBus } from 'n8n-design-system/utils'; import Modal from './Modal.vue'; import { CHAT_EMBED_MODAL_KEY, WEBHOOK_NODE_TYPE } from '../constants'; -import { useSettingsStore } from '@/stores/settings.store'; import { useRootStore } from '@/stores/n8nRoot.store'; import { useWorkflowsStore } from '@/stores/workflows.store'; import HtmlEditor from '@/components/HtmlEditor/HtmlEditor.vue'; @@ -22,7 +21,6 @@ const props = defineProps({ const i18n = useI18n(); const rootStore = useRootStore(); const workflowsStore = useWorkflowsStore(); -const settingsStore = useSettingsStore(); const tabs = ref([ { diff --git a/packages/editor-ui/src/components/CodeNodeEditor/completions/secrets.completions.ts b/packages/editor-ui/src/components/CodeNodeEditor/completions/secrets.completions.ts index 79bbe2b211..7bba9b3fa3 100644 --- a/packages/editor-ui/src/components/CodeNodeEditor/completions/secrets.completions.ts +++ b/packages/editor-ui/src/components/CodeNodeEditor/completions/secrets.completions.ts @@ -1,12 +1,11 @@ -import Vue from 'vue'; import { addVarType } from '../utils'; import type { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete'; -import type { CodeNodeEditorMixin } from '../types'; import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store'; +import { defineComponent } from 'vue'; const escape = (str: string) => str.replace('$', '\\$'); -export const secretsCompletions = (Vue as CodeNodeEditorMixin).extend({ +export const secretsCompletions = defineComponent({ methods: { /** * Complete `$secrets.` to `$secrets.providerName` and `$secrets.providerName.secretName`. diff --git a/packages/editor-ui/src/components/Error/NodeErrorView.vue b/packages/editor-ui/src/components/Error/NodeErrorView.vue index 9f2d2858d6..eda0d667d5 100644 --- a/packages/editor-ui/src/components/Error/NodeErrorView.vue +++ b/packages/editor-ui/src/components/Error/NodeErrorView.vue @@ -1,7 +1,7 @@