mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-14 08:34:07 -08:00
Merge remote-tracking branch 'origin/master' into export-import
This commit is contained in:
commit
5eb9413469
|
@ -11,3 +11,8 @@
|
|||
# refactor: Run lintfix (no-changelog) (#7537)
|
||||
|
||||
62c096710fab2f7e886518abdbded34b55e93f62
|
||||
|
||||
# refactor: Move test files alongside tested files (#11504)
|
||||
|
||||
7e58fc4fec468aca0b45d5bfe6150e1af632acbc
|
||||
f32b13c6ed078be042a735bc8621f27e00dc3116
|
||||
|
|
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
|
@ -11,6 +11,8 @@ Photos and videos are recommended.
|
|||
Include links to **Linear ticket** or Github issue or Community forum post.
|
||||
Important in order to close *automatically* and provide context to reviewers.
|
||||
-->
|
||||
<!-- Use "closes #<issue-number>", "fixes #<issue-number>", or "resolves #<issue-number>" to automatically close issues when the PR is merged. -->
|
||||
|
||||
|
||||
## Review / Merge checklist
|
||||
|
||||
|
|
1
.github/workflows/chromatic.yml
vendored
1
.github/workflows/chromatic.yml
vendored
|
@ -65,6 +65,7 @@ jobs:
|
|||
continue-on-error: true
|
||||
with:
|
||||
workingDir: packages/design-system
|
||||
onlyChanged: true
|
||||
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||
exitZeroOnChanges: false
|
||||
|
||||
|
|
3
.github/workflows/ci-pull-requests.yml
vendored
3
.github/workflows/ci-pull-requests.yml
vendored
|
@ -49,6 +49,9 @@ jobs:
|
|||
with:
|
||||
ref: refs/pull/${{ github.event.pull_request.number }}/merge
|
||||
cacheKey: ${{ github.sha }}-base:build
|
||||
collectCoverage: true
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
|
|
6
.github/workflows/e2e-reusable.yml
vendored
6
.github/workflows/e2e-reusable.yml
vendored
|
@ -41,6 +41,11 @@ on:
|
|||
description: 'PR number to run tests for.'
|
||||
required: false
|
||||
type: number
|
||||
node_view_version:
|
||||
description: 'Node View version to run tests with.'
|
||||
required: false
|
||||
default: '1'
|
||||
type: string
|
||||
secrets:
|
||||
CYPRESS_RECORD_KEY:
|
||||
description: 'Cypress record key.'
|
||||
|
@ -160,6 +165,7 @@ jobs:
|
|||
spec: '${{ inputs.spec }}'
|
||||
env:
|
||||
NODE_OPTIONS: --dns-result-order=ipv4first
|
||||
CYPRESS_NODE_VIEW_VERSION: ${{ inputs.node_view_version }}
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
E2E_TESTS: true
|
||||
|
|
6
.github/workflows/e2e-tests.yml
vendored
6
.github/workflows/e2e-tests.yml
vendored
|
@ -27,6 +27,11 @@ on:
|
|||
description: 'URL to call after workflow is done.'
|
||||
required: false
|
||||
default: ''
|
||||
node_view_version:
|
||||
description: 'Node View version to run tests with.'
|
||||
required: false
|
||||
default: '1'
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
calls-start-url:
|
||||
|
@ -46,6 +51,7 @@ jobs:
|
|||
branch: ${{ github.event.inputs.branch || 'master' }}
|
||||
user: ${{ github.event.inputs.user || 'PR User' }}
|
||||
spec: ${{ github.event.inputs.spec || 'e2e/*' }}
|
||||
node_view_version: ${{ github.event.inputs.node_view_version || '1' }}
|
||||
secrets:
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
|
||||
|
|
40
.github/workflows/release-publish.yml
vendored
40
.github/workflows/release-publish.yml
vendored
|
@ -38,6 +38,12 @@ jobs:
|
|||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Cache build artifacts
|
||||
uses: actions/cache/save@v4.0.0
|
||||
with:
|
||||
path: ./packages/**/dist
|
||||
key: ${{ github.sha }}-release:build
|
||||
|
||||
- name: Dry-run publishing
|
||||
run: pnpm publish -r --no-git-checks --dry-run
|
||||
|
||||
|
@ -119,6 +125,40 @@ jobs:
|
|||
makeLatest: false
|
||||
body: ${{github.event.pull_request.body}}
|
||||
|
||||
create-sentry-release:
|
||||
name: Create a Sentry Release
|
||||
needs: [publish-to-npm, publish-to-docker-hub]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.merged == true
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
- name: Restore cached build artifacts
|
||||
uses: actions/cache/restore@v4.0.0
|
||||
with:
|
||||
path: ./packages/**/dist
|
||||
key: ${{ github.sha }}-release:build
|
||||
|
||||
- name: Create a frontend release
|
||||
uses: getsentry/action-release@v1.7.0
|
||||
continue-on-error: true
|
||||
with:
|
||||
projects: ${{ secrets.SENTRY_FRONTEND_PROJECT }}
|
||||
version: ${{ needs.publish-to-npm.outputs.release }}
|
||||
sourcemaps: packages/editor-ui/dist
|
||||
|
||||
- name: Create a backend release
|
||||
uses: getsentry/action-release@v1.7.0
|
||||
continue-on-error: true
|
||||
with:
|
||||
projects: ${{ secrets.SENTRY_BACKEND_PROJECT }}
|
||||
version: ${{ needs.publish-to-npm.outputs.release }}
|
||||
sourcemaps: packages/cli/dist packages/core/dist packages/nodes-base/dist packages/@n8n/n8n-nodes-langchain/dist
|
||||
|
||||
trigger-release-note:
|
||||
name: Trigger a release note
|
||||
needs: [publish-to-npm, create-github-release]
|
||||
|
|
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
|
@ -7,6 +7,7 @@
|
|||
"EditorConfig.EditorConfig",
|
||||
"esbenp.prettier-vscode",
|
||||
"mjmlio.vscode-mjml",
|
||||
"Vue.volar"
|
||||
"Vue.volar",
|
||||
"vitest.explorer"
|
||||
]
|
||||
}
|
||||
|
|
171
CHANGELOG.md
171
CHANGELOG.md
|
@ -1,3 +1,174 @@
|
|||
# [1.67.0](https://github.com/n8n-io/n8n/compare/n8n@1.66.0...n8n@1.67.0) (2024-11-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Bring back nodes panel telemetry events ([#11456](https://github.com/n8n-io/n8n/issues/11456)) ([130c942](https://github.com/n8n-io/n8n/commit/130c942f633788d1b2f937d6fea342d4450c6e3d))
|
||||
* **core:** Account for double quotes in instance base URL ([#11495](https://github.com/n8n-io/n8n/issues/11495)) ([c5191e6](https://github.com/n8n-io/n8n/commit/c5191e697a9a9ebfa2b67587cd01b5835ebf6ea8))
|
||||
* **core:** Do not delete waiting executions when saving of successful executions is disabled ([#11458](https://github.com/n8n-io/n8n/issues/11458)) ([e8757e5](https://github.com/n8n-io/n8n/commit/e8757e58f69e091ac3d2a2f8e8c8e33ac57c1e47))
|
||||
* **core:** Don't send a `executionFinished` event to the browser with no run data if the execution has already been cleaned up ([#11502](https://github.com/n8n-io/n8n/issues/11502)) ([d1153f5](https://github.com/n8n-io/n8n/commit/d1153f51e80911cbc8f34ba5f038f349b75295c3))
|
||||
* **core:** Include `projectId` in range query middleware ([#11590](https://github.com/n8n-io/n8n/issues/11590)) ([a6070af](https://github.com/n8n-io/n8n/commit/a6070afdda29631fd36e5213f52bf815268bcda4))
|
||||
* **core:** Save exeution progress for waiting executions, even when progress saving is disabled ([#11535](https://github.com/n8n-io/n8n/issues/11535)) ([6b9353c](https://github.com/n8n-io/n8n/commit/6b9353c80f61ab36945fff434d98242dc1cab7b3))
|
||||
* **core:** Use the correct docs URL for regular nodes when used as tools ([#11529](https://github.com/n8n-io/n8n/issues/11529)) ([a092b8e](https://github.com/n8n-io/n8n/commit/a092b8e972e1253d92df416f19096a045858e7c1))
|
||||
* **Edit Image Node:** Fix Text operation by setting Arial as default font ([#11125](https://github.com/n8n-io/n8n/issues/11125)) ([60c1ace](https://github.com/n8n-io/n8n/commit/60c1ace64be29d651ce7b777fbb576598e38b9d7))
|
||||
* **editor:** Auto focus first fields on SignIn, SignUp and ForgotMyPassword views ([#11445](https://github.com/n8n-io/n8n/issues/11445)) ([5b5bd72](https://github.com/n8n-io/n8n/commit/5b5bd7291dde17880b7699f7e6832938599ffd8f))
|
||||
* **editor:** Do not overwrite the webhookId in the new canvas ([#11562](https://github.com/n8n-io/n8n/issues/11562)) ([dfd785b](https://github.com/n8n-io/n8n/commit/dfd785bc0894257eb6e62b0dd8f71248c27aae53))
|
||||
* **editor:** Ensure Enter key on Cancel button correctly cancels node rename ([#11563](https://github.com/n8n-io/n8n/issues/11563)) ([be05ae3](https://github.com/n8n-io/n8n/commit/be05ae36e7790156cb48b317fc254ae46a3b2d8c))
|
||||
* **editor:** Fix emitting `n8nReady` notification via `postmessage` on new canvas ([#11558](https://github.com/n8n-io/n8n/issues/11558)) ([463d101](https://github.com/n8n-io/n8n/commit/463d101f3592e6df4afd66c4d0fde0cb4aec34cc))
|
||||
* **editor:** Fix run index input for RunData view in sub-nodes ([#11538](https://github.com/n8n-io/n8n/issues/11538)) ([434d31c](https://github.com/n8n-io/n8n/commit/434d31ce928342d52b6ab8b78639afd7829216d4))
|
||||
* **editor:** Fix selected credential being overwritten in NDV ([#11496](https://github.com/n8n-io/n8n/issues/11496)) ([a26c0e2](https://github.com/n8n-io/n8n/commit/a26c0e2c3c7da87bfaba9737a967aa0070810d85))
|
||||
* **editor:** Keep workflow pristine after load on new canvas ([#11579](https://github.com/n8n-io/n8n/issues/11579)) ([7254359](https://github.com/n8n-io/n8n/commit/7254359855b89769613cd5cc24dbb4f45a7cc76f))
|
||||
* Show Pinned data in demo mode ([#11490](https://github.com/n8n-io/n8n/issues/11490)) ([ca2a583](https://github.com/n8n-io/n8n/commit/ca2a583b5cbb0cba3ecb694261806de16547aa91))
|
||||
* Toast not aligned to the bottom when AI assistant disable ([#11549](https://github.com/n8n-io/n8n/issues/11549)) ([e80f7e0](https://github.com/n8n-io/n8n/commit/e80f7e0a02a972379f73af6a44de11768081086e))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add Rapid7 InsightVm credentials ([#11462](https://github.com/n8n-io/n8n/issues/11462)) ([46eceab](https://github.com/n8n-io/n8n/commit/46eceabc27ac219b11b85c16c533a2cff848c5dd))
|
||||
* **AI Transform Node:** UX improvements ([#11280](https://github.com/n8n-io/n8n/issues/11280)) ([8a48407](https://github.com/n8n-io/n8n/commit/8a484077af3d3e1fe2d1b90b1ea9edf4ba41fcb6))
|
||||
* **Anthropic Chat Model Node:** Add support for Haiku 3.5 ([#11551](https://github.com/n8n-io/n8n/issues/11551)) ([8b39825](https://github.com/n8n-io/n8n/commit/8b398256a81594a52f20f8eb8adf8ff205209bc1))
|
||||
* **Convert to File Node:** Add delimiter convert to csv ([#11556](https://github.com/n8n-io/n8n/issues/11556)) ([63d454b](https://github.com/n8n-io/n8n/commit/63d454b776c092ff8c6c521a7e083774adb8f649))
|
||||
* **editor:** Update panning and selection keybindings on new canvas ([#11534](https://github.com/n8n-io/n8n/issues/11534)) ([5e2e205](https://github.com/n8n-io/n8n/commit/5e2e205394adf76faf02aee2d4f21df71848e1d4))
|
||||
* **Gmail Trigger Node:** Add filter option to include drafts ([#11441](https://github.com/n8n-io/n8n/issues/11441)) ([7a2be77](https://github.com/n8n-io/n8n/commit/7a2be77f384a32ede3acad8fe24fb89227c058bf))
|
||||
* **Intercom Node:** Update credential to new style ([#11485](https://github.com/n8n-io/n8n/issues/11485)) ([b137e13](https://github.com/n8n-io/n8n/commit/b137e13845f0714ebf7421c837f5ab104b66709b))
|
||||
|
||||
|
||||
|
||||
# [1.66.0](https://github.com/n8n-io/n8n/compare/n8n@1.65.0...n8n@1.66.0) (2024-10-31)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Asana Node:** Fix issue with pagination ([#11415](https://github.com/n8n-io/n8n/issues/11415)) ([04c075a](https://github.com/n8n-io/n8n/commit/04c075a46bcc7b1964397f0244b0fde99476212d))
|
||||
* **core:** Add 'user_id' to `license-community-plus-registered` telemetry event ([#11430](https://github.com/n8n-io/n8n/issues/11430)) ([7a8dafe](https://github.com/n8n-io/n8n/commit/7a8dafe9902fbc0d5001c50579c34959b95211ab))
|
||||
* **core:** Add safeguard for command publishing ([#11337](https://github.com/n8n-io/n8n/issues/11337)) ([656439e](https://github.com/n8n-io/n8n/commit/656439e87138f9f96dea5a683cfdac3f661ffefb))
|
||||
* **core:** Ensure `LoggerProxy` is not scoped ([#11379](https://github.com/n8n-io/n8n/issues/11379)) ([f4ea943](https://github.com/n8n-io/n8n/commit/f4ea943c9cb2321e41705de6c5c27535a0f5eae0))
|
||||
* **core:** Ensure `remove-triggers-and-pollers` command is not debounced ([#11486](https://github.com/n8n-io/n8n/issues/11486)) ([529d4fc](https://github.com/n8n-io/n8n/commit/529d4fc3ef5206bd1b02d27634342cc50b45997e))
|
||||
* **core:** Ensure job processor does not reprocess amended executions ([#11438](https://github.com/n8n-io/n8n/issues/11438)) ([c152a3a](https://github.com/n8n-io/n8n/commit/c152a3ac56f140a39eea4771a94f5a3082118df7))
|
||||
* **core:** Fix Message Event Bus Metrics not counting up for labeled metrics ([#11396](https://github.com/n8n-io/n8n/issues/11396)) ([7fc3b25](https://github.com/n8n-io/n8n/commit/7fc3b25d21c6c4f1802f34b1ae065a65cac3001b))
|
||||
* **core:** Fix resolving of $fromAI expression via `evaluateExpression` ([#11397](https://github.com/n8n-io/n8n/issues/11397)) ([2e64464](https://github.com/n8n-io/n8n/commit/2e6446454defbd3e5a47b66e6fd46d4f1b9fbd0f))
|
||||
* **core:** Make execution and its data creation atomic ([#11392](https://github.com/n8n-io/n8n/issues/11392)) ([ed30d43](https://github.com/n8n-io/n8n/commit/ed30d43236bf3c6b657022636a02a41be01aa152))
|
||||
* **core:** On unhandled rejections, extract the original exception correctly ([#11389](https://github.com/n8n-io/n8n/issues/11389)) ([8608bae](https://github.com/n8n-io/n8n/commit/8608baeb7ec302daddc8adca6e39778dcf7b2eda))
|
||||
* **editor:** Fix TypeError: Cannot read properties of undefined (reading '0') ([#11399](https://github.com/n8n-io/n8n/issues/11399)) ([ae37c52](https://github.com/n8n-io/n8n/commit/ae37c520a91c75e353e818944b36a3619c0d8b4a))
|
||||
* **editor:** Add Retry button for AI Assistant errors ([#11345](https://github.com/n8n-io/n8n/issues/11345)) ([7699240](https://github.com/n8n-io/n8n/commit/7699240073122cdef31cf109fd37fa66961f588a))
|
||||
* **editor:** Change tooltip for workflow with execute workflow trigger ([#11374](https://github.com/n8n-io/n8n/issues/11374)) ([dcd6038](https://github.com/n8n-io/n8n/commit/dcd6038c3085135803cdaa546a239359a6d449eb))
|
||||
* **editor:** Ensure toasts show above modal overlays ([#11410](https://github.com/n8n-io/n8n/issues/11410)) ([351134f](https://github.com/n8n-io/n8n/commit/351134f786af933f5f310bf8d9897269387635a0))
|
||||
* **editor:** Fit view consistently after nodes are initialized on new canvas ([#11457](https://github.com/n8n-io/n8n/issues/11457)) ([497d637](https://github.com/n8n-io/n8n/commit/497d637fc5308b9c4a06bc764152fde1f1a9c130))
|
||||
* **editor:** Fix adding connections when initializing workspace in templates view on new canvas ([#11451](https://github.com/n8n-io/n8n/issues/11451)) ([ea47b02](https://github.com/n8n-io/n8n/commit/ea47b025fb16c967d4fc73dcacc6e260d2aecd61))
|
||||
* **editor:** Fix rendering of AI logs ([#11450](https://github.com/n8n-io/n8n/issues/11450)) ([73b0a80](https://github.com/n8n-io/n8n/commit/73b0a80ac92b4f4b5a300d0ec1c833b4395a222a))
|
||||
* **editor:** Hide data mapping tooltip in credential edit modal ([#11356](https://github.com/n8n-io/n8n/issues/11356)) ([ff14dcb](https://github.com/n8n-io/n8n/commit/ff14dcb3a1ddaea4eca7c1ecd2e92c0abb0c413c))
|
||||
* **editor:** Prevent running workflow that has issues if listening to webhook ([#11402](https://github.com/n8n-io/n8n/issues/11402)) ([8b0a48f](https://github.com/n8n-io/n8n/commit/8b0a48f53010378e497e4cc362fda75a958cf363))
|
||||
* **editor:** Run external hooks after settings have been initialized ([#11423](https://github.com/n8n-io/n8n/issues/11423)) ([0ab24c8](https://github.com/n8n-io/n8n/commit/0ab24c814abd1787268750ba808993ab2735ac52))
|
||||
* **editor:** Support middle click to scroll when using a mouse on new canvas ([#11384](https://github.com/n8n-io/n8n/issues/11384)) ([46f3b4a](https://github.com/n8n-io/n8n/commit/46f3b4a258f89f02e0d2bd1eef25a22e3a721167))
|
||||
* **HTTP Request Tool Node:** Fix HTML response optimization issue ([#11439](https://github.com/n8n-io/n8n/issues/11439)) ([cf37e94](https://github.com/n8n-io/n8n/commit/cf37e94dd875e9f6ab1f189146fb34e7296af93c))
|
||||
* **n8n Form Node:** Form Trigger does not wait in multi-form mode ([#11404](https://github.com/n8n-io/n8n/issues/11404)) ([151f4dd](https://github.com/n8n-io/n8n/commit/151f4dd7b8f800af424f8ae64cb8238975fb3cb8))
|
||||
* Update required node js version in CONTRIBUTING.md ([#11437](https://github.com/n8n-io/n8n/issues/11437)) ([4f511aa](https://github.com/n8n-io/n8n/commit/4f511aab68651caa8fe47f70cd7cdb88bb06a3e2))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Anthropic Chat Model Node:** Add model claude-3-5-sonnet-20241022 ([#11465](https://github.com/n8n-io/n8n/issues/11465)) ([f6c8890](https://github.com/n8n-io/n8n/commit/f6c8890a8069de221b9b96e735418ecc9624cf7b))
|
||||
* **core:** Handle nodes with multiple inputs and connections during partial executions ([#11376](https://github.com/n8n-io/n8n/issues/11376)) ([cb7c4d2](https://github.com/n8n-io/n8n/commit/cb7c4d29a6f042b590822e5b9c67fff0a8f0863d))
|
||||
* **editor:** Add descriptive header to projects /workflow ([#11203](https://github.com/n8n-io/n8n/issues/11203)) ([5d19e8f](https://github.com/n8n-io/n8n/commit/5d19e8f2b45dc1abc5a8253f9e3a0fdacb1ebd91))
|
||||
* **editor:** Improve placeholder for vector store tool ([#11483](https://github.com/n8n-io/n8n/issues/11483)) ([629e092](https://github.com/n8n-io/n8n/commit/629e09240785bc648ff6575f97910fbb4e77cdab))
|
||||
* **editor:** Remove edge execution animation on new canvas ([#11446](https://github.com/n8n-io/n8n/issues/11446)) ([a701d87](https://github.com/n8n-io/n8n/commit/a701d87f5ba94ffc811e424b60e188b26ac6c1c5))
|
||||
* **editor:** Update ownership pills ([#11155](https://github.com/n8n-io/n8n/issues/11155)) ([8147038](https://github.com/n8n-io/n8n/commit/8147038cf87dca657602e617e49698065bf1a63f))
|
||||
|
||||
|
||||
|
||||
# [1.65.0](https://github.com/n8n-io/n8n/compare/n8n@1.64.0...n8n@1.65.0) (2024-10-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **AI Agent Node:** Preserve `intermediateSteps` when using output parser with non-tool agent ([#11363](https://github.com/n8n-io/n8n/issues/11363)) ([e61a853](https://github.com/n8n-io/n8n/commit/e61a8535aa39653b9a87575ea911a65318282167))
|
||||
* **API:** `PUT /credentials/:id` should move the specified credential, not the first one in the database ([#11365](https://github.com/n8n-io/n8n/issues/11365)) ([e6b2f8e](https://github.com/n8n-io/n8n/commit/e6b2f8e7e6ebbb6e3776a976297d519e99ac6c64))
|
||||
* **API:** Correct credential schema for response in `POST /credentials` ([#11340](https://github.com/n8n-io/n8n/issues/11340)) ([f495875](https://github.com/n8n-io/n8n/commit/f4958756b4976e0b608b9155dab84564f7e8804e))
|
||||
* **core:** Account for waiting jobs during shutdown ([#11338](https://github.com/n8n-io/n8n/issues/11338)) ([c863abd](https://github.com/n8n-io/n8n/commit/c863abd08300b53ea898fc4d06aae97dec7afa9b))
|
||||
* **core:** Add missing primary key to execution annotation tags table ([#11168](https://github.com/n8n-io/n8n/issues/11168)) ([b4b543d](https://github.com/n8n-io/n8n/commit/b4b543d41daa07753eca24ab93bf7445f672361d))
|
||||
* **core:** Change dedupe value column type from varchar(255) to text ([#11357](https://github.com/n8n-io/n8n/issues/11357)) ([7a71cff](https://github.com/n8n-io/n8n/commit/7a71cff4d75fe4e7282a398b4843428e0161ba8c))
|
||||
* **core:** Do not debounce webhooks, triggers and pollers activation ([#11306](https://github.com/n8n-io/n8n/issues/11306)) ([64bddf8](https://github.com/n8n-io/n8n/commit/64bddf86536ddd688638a643d24f80c947a12f31))
|
||||
* **core:** Enforce nodejs version consistently ([#11323](https://github.com/n8n-io/n8n/issues/11323)) ([0fa2e8c](https://github.com/n8n-io/n8n/commit/0fa2e8ca85005362d9043d82469f3c3525f4c4ef))
|
||||
* **core:** Fix memory issue with empty model response ([#11300](https://github.com/n8n-io/n8n/issues/11300)) ([216b119](https://github.com/n8n-io/n8n/commit/216b119350949de70f15cf2d61f474770803ad7a))
|
||||
* **core:** Fix race condition when resolving post-execute promise ([#11360](https://github.com/n8n-io/n8n/issues/11360)) ([4f1816e](https://github.com/n8n-io/n8n/commit/4f1816e03db00219bc2e723e3048848aef7f8fe1))
|
||||
* **core:** Sanitise IdP provided information in SAML test pages ([#11171](https://github.com/n8n-io/n8n/issues/11171)) ([74fc388](https://github.com/n8n-io/n8n/commit/74fc3889b946e8f224e65ef8d3d44125404aa4fc))
|
||||
* Don't show pin button in input panel when there's binary data ([#11267](https://github.com/n8n-io/n8n/issues/11267)) ([c0b5b92](https://github.com/n8n-io/n8n/commit/c0b5b92f62a2d7ba60492eb27daced268b654fe9))
|
||||
* **editor:** Add Personal project to main navigation ([#11161](https://github.com/n8n-io/n8n/issues/11161)) ([1f441f9](https://github.com/n8n-io/n8n/commit/1f441f97528f58e905eaf8930577bbcd08debf06))
|
||||
* **editor:** Fix Cannot read properties of undefined (reading 'finished') ([#11367](https://github.com/n8n-io/n8n/issues/11367)) ([475d72e](https://github.com/n8n-io/n8n/commit/475d72e0bc9e13c6dc56129902f6f89c67547f78))
|
||||
* **editor:** Fix delete all existing executions ([#11352](https://github.com/n8n-io/n8n/issues/11352)) ([3ec103f](https://github.com/n8n-io/n8n/commit/3ec103f8baaa89e579844947d945f00bec9e498e))
|
||||
* **editor:** Fix pin data button disappearing after reload ([#11198](https://github.com/n8n-io/n8n/issues/11198)) ([3b2f63e](https://github.com/n8n-io/n8n/commit/3b2f63e248cd0cba04087e2f40e13d670073707d))
|
||||
* **editor:** Fix RunData non-binary pagination when binary data is present ([#11309](https://github.com/n8n-io/n8n/issues/11309)) ([901888d](https://github.com/n8n-io/n8n/commit/901888d5b1027098653540c72f787f176941f35a))
|
||||
* **editor:** Fix sorting problem in older browsers that don't support `toSorted` ([#11204](https://github.com/n8n-io/n8n/issues/11204)) ([c728a2f](https://github.com/n8n-io/n8n/commit/c728a2ffe01f510a237979a54897c4680a407800))
|
||||
* **editor:** Follow-up fixes to projects side menu ([#11327](https://github.com/n8n-io/n8n/issues/11327)) ([4dde772](https://github.com/n8n-io/n8n/commit/4dde772814c55e66efcc9b369ae443328af21b14))
|
||||
* **editor:** Keep always focus on the first item on the node's search panel ([#11193](https://github.com/n8n-io/n8n/issues/11193)) ([c57cac9](https://github.com/n8n-io/n8n/commit/c57cac9e4d447c3a4240a565f9f2de8aa3b7c513))
|
||||
* **editor:** Open Community+ enrollment modal only for the instance owner ([#11292](https://github.com/n8n-io/n8n/issues/11292)) ([76724c3](https://github.com/n8n-io/n8n/commit/76724c3be6e001792433045c2b2aac0ef16d4b8a))
|
||||
* **editor:** Record sessionStarted telemetry event in Setting Store ([#11334](https://github.com/n8n-io/n8n/issues/11334)) ([1b734dd](https://github.com/n8n-io/n8n/commit/1b734dd9f42885594ce02400cfb395a4f5e7e088))
|
||||
* Ensure NDV params don't get cut off early and scrolled to the top ([#11252](https://github.com/n8n-io/n8n/issues/11252)) ([054fe97](https://github.com/n8n-io/n8n/commit/054fe9745ff6864f9088aa4cd66ed9e7869520d5))
|
||||
* **HTTP Request Tool Node:** Fix the undefined response issue when authentication is enabled ([#11343](https://github.com/n8n-io/n8n/issues/11343)) ([094ec68](https://github.com/n8n-io/n8n/commit/094ec68d4c00848013aa4eec4ac5efbd2c92afc5))
|
||||
* Include error in the message in JS task runner sandbox ([#11359](https://github.com/n8n-io/n8n/issues/11359)) ([0708b3a](https://github.com/n8n-io/n8n/commit/0708b3a1f8097af829c92fe106ea6ba375d6c500))
|
||||
* **Microsoft SQL Node:** Fix execute query to allow for non select query to run ([#11335](https://github.com/n8n-io/n8n/issues/11335)) ([ba158b4](https://github.com/n8n-io/n8n/commit/ba158b4f8533bd3430db8766d4921f75db5c1a11))
|
||||
* **OpenAI Chat Model Node, Ollama Chat Model Node:** Change default model to a more up-to-date option ([#11293](https://github.com/n8n-io/n8n/issues/11293)) ([0be04c6](https://github.com/n8n-io/n8n/commit/0be04c6348d8c059a96c3d37a6d6cd587bfb97f3))
|
||||
* **Pinecone Vector Store Node:** Prevent populating of vectors after manually stopping the execution ([#11288](https://github.com/n8n-io/n8n/issues/11288)) ([fbae17d](https://github.com/n8n-io/n8n/commit/fbae17d8fb35a5197fa183e3639bb36762dc73d2))
|
||||
* **Postgres Node:** Special datetime values cause errors ([#11225](https://github.com/n8n-io/n8n/issues/11225)) ([3c57f46](https://github.com/n8n-io/n8n/commit/3c57f46aaeb968d2974f2dc9790317a6a6fab624))
|
||||
* Resend invite operation on users list ([#11351](https://github.com/n8n-io/n8n/issues/11351)) ([e4218de](https://github.com/n8n-io/n8n/commit/e4218debd18812fa3aa508339afd3de03c4d69dc))
|
||||
* **SSH Node:** Cleanup temporary binary files as soon as possible ([#11305](https://github.com/n8n-io/n8n/issues/11305)) ([08a7b5b](https://github.com/n8n-io/n8n/commit/08a7b5b7425663ec6593114921c2e22ab37d039e))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add report bug buttons ([#11304](https://github.com/n8n-io/n8n/issues/11304)) ([296f68f](https://github.com/n8n-io/n8n/commit/296f68f041b93fd32ac7be2b53c2b41d58c2998a))
|
||||
* **AI Agent Node:** Make tools optional when using OpenAI model with Tools agent ([#11212](https://github.com/n8n-io/n8n/issues/11212)) ([fed7c3e](https://github.com/n8n-io/n8n/commit/fed7c3ec1fb0553adaa9a933f91aabfd54fe83a3))
|
||||
* **core:** introduce JWT API keys for the public API ([#11005](https://github.com/n8n-io/n8n/issues/11005)) ([679fa4a](https://github.com/n8n-io/n8n/commit/679fa4a10a85fc96e12ca66fe12cdb32368bc12b))
|
||||
* **core:** Enforce config file permissions on startup ([#11328](https://github.com/n8n-io/n8n/issues/11328)) ([c078a51](https://github.com/n8n-io/n8n/commit/c078a516bec857831cc904ef807d0791b889f3a2))
|
||||
* **core:** Handle cycles in workflows when partially executing them ([#11187](https://github.com/n8n-io/n8n/issues/11187)) ([321d6de](https://github.com/n8n-io/n8n/commit/321d6deef18806d88d97afef2f2c6f29e739ccb4))
|
||||
* **editor:** Separate node output execution tooltip from status icon ([#11196](https://github.com/n8n-io/n8n/issues/11196)) ([cd15e95](https://github.com/n8n-io/n8n/commit/cd15e959c7af82a7d8c682e94add2b2640624a70))
|
||||
* **GitHub Node:** Add workflow resource operations ([#10744](https://github.com/n8n-io/n8n/issues/10744)) ([d309112](https://github.com/n8n-io/n8n/commit/d3091126472faa2c8f270650e54027d19dc56bb6))
|
||||
* **n8n Form Page Node:** New node ([#10390](https://github.com/n8n-io/n8n/issues/10390)) ([643d66c](https://github.com/n8n-io/n8n/commit/643d66c0ae084a0d93dac652703adc0a32cab8de))
|
||||
* **n8n Google My Business Node:** New node ([#10504](https://github.com/n8n-io/n8n/issues/10504)) ([bf28fbe](https://github.com/n8n-io/n8n/commit/bf28fbefe5e8ba648cba1555a2d396b75ee32bbb))
|
||||
* Run `mfa.beforeSetup` hook before enabling MFA ([#11116](https://github.com/n8n-io/n8n/issues/11116)) ([25c1c32](https://github.com/n8n-io/n8n/commit/25c1c3218cf1075ca3abd961236f3b2fbd9d6ba9))
|
||||
* **Structured Output Parser Node:** Refactor Output Parsers and Improve Error Handling ([#11148](https://github.com/n8n-io/n8n/issues/11148)) ([45274f2](https://github.com/n8n-io/n8n/commit/45274f2e7f081e194e330e1c9e6a5c26fca0b141))
|
||||
|
||||
|
||||
|
||||
# [1.64.0](https://github.com/n8n-io/n8n/compare/n8n@1.63.0...n8n@1.64.0) (2024-10-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Adjust arrow button colors in dark mode ([#11248](https://github.com/n8n-io/n8n/issues/11248)) ([439132c](https://github.com/n8n-io/n8n/commit/439132c291a812d57702c94eaa12878394ac4c69))
|
||||
* **core:** Ensure error reporter does not promote `info` to `error` messages ([#11245](https://github.com/n8n-io/n8n/issues/11245)) ([a7fc7fc](https://github.com/n8n-io/n8n/commit/a7fc7fc22997acec86dc94386c95349fd018f4ae))
|
||||
* **core:** Override executions mode if `regular` during worker startup ([#11250](https://github.com/n8n-io/n8n/issues/11250)) ([c0aa28c](https://github.com/n8n-io/n8n/commit/c0aa28c6cf3f77b04e04663217c9df8e3803ed3f))
|
||||
* **core:** Wrap nodes for tool use at a suitable time ([#11238](https://github.com/n8n-io/n8n/issues/11238)) ([c2fb881](https://github.com/n8n-io/n8n/commit/c2fb881d61291209802438d95892d052f5c82d43))
|
||||
* Don't show pinned data tooltip for pinned nodes ([#11249](https://github.com/n8n-io/n8n/issues/11249)) ([c2ad156](https://github.com/n8n-io/n8n/commit/c2ad15646d326a8f71e314d54efe202a5bcdd296))
|
||||
* **editor:** Bring back the "Forgot password" link on SigninView ([#11216](https://github.com/n8n-io/n8n/issues/11216)) ([4e78c46](https://github.com/n8n-io/n8n/commit/4e78c46a7450c7fc0694369944d4fb446cef2348))
|
||||
* **editor:** Fix chat crashing when rendering output-parsed content ([#11210](https://github.com/n8n-io/n8n/issues/11210)) ([4aaebfd](https://github.com/n8n-io/n8n/commit/4aaebfd4358f590e98c453ad4e65cc2c9d0f76f8))
|
||||
* **editor:** Make submit in ChangePasswordView work again ([#11227](https://github.com/n8n-io/n8n/issues/11227)) ([4f27b39](https://github.com/n8n-io/n8n/commit/4f27b39b45b58779d363980241e6e5e11b58f5da))
|
||||
* Expressions display actual result of evaluating expression inside string ([#11257](https://github.com/n8n-io/n8n/issues/11257)) ([7f5f0a9](https://github.com/n8n-io/n8n/commit/7f5f0a9df3b3fae6e2f9787443ac1cf9415d5932))
|
||||
* **Google Ads Node:** Update to use v17 api ([#11243](https://github.com/n8n-io/n8n/issues/11243)) ([3d97f02](https://github.com/n8n-io/n8n/commit/3d97f02a8d2b6e5bc7c97c5271bed97417ecacd2))
|
||||
* **Google Calendar Node:** Fix issue with conference data types not loading ([#11185](https://github.com/n8n-io/n8n/issues/11185)) ([4012758](https://github.com/n8n-io/n8n/commit/401275884e5db0287e4eeffb3c7497dd5e024880))
|
||||
* **Google Calendar Node:** Mode to add or replace attendees in event update ([#11132](https://github.com/n8n-io/n8n/issues/11132)) ([6c6a8ef](https://github.com/n8n-io/n8n/commit/6c6a8efdea83cf7194304ce089d7b72d8f6c1a9d))
|
||||
* **HTTP Request Tool Node:** Respond with an error when receive binary response ([#11219](https://github.com/n8n-io/n8n/issues/11219)) ([0d23a7f](https://github.com/n8n-io/n8n/commit/0d23a7fb5ba41545f70c4848d30b90af91b1e7e6))
|
||||
* **MySQL Node:** Fix "Maximum call stack size exceeded" error when handling a large number of rows ([#11242](https://github.com/n8n-io/n8n/issues/11242)) ([b7ee0c4](https://github.com/n8n-io/n8n/commit/b7ee0c4087eae346bc7e5360130d6c812dbe99db))
|
||||
* **n8n Trigger Node:** Merge with Workflow Trigger node ([#11174](https://github.com/n8n-io/n8n/issues/11174)) ([6ec6b51](https://github.com/n8n-io/n8n/commit/6ec6b5197ae97eb86496effd458fcc0b9b223ef3))
|
||||
* **OpenAI Node:** Fix tool parameter parsing issue ([#11201](https://github.com/n8n-io/n8n/issues/11201)) ([5a1d81a](https://github.com/n8n-io/n8n/commit/5a1d81ad917fde5cd6a387fe2d4ec6aab6b71349))
|
||||
* **Set Node:** Fix issue with UI properties not being hidden ([#11263](https://github.com/n8n-io/n8n/issues/11263)) ([1affc27](https://github.com/n8n-io/n8n/commit/1affc27b6bf9a559061a06f92bebe8167d938665))
|
||||
* **Strava Trigger Node:** Fix issue with webhook not being deleted ([#11226](https://github.com/n8n-io/n8n/issues/11226)) ([566529c](https://github.com/n8n-io/n8n/commit/566529ca1149988a54a58b3c34bbe4d9f1add6db))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add tracking for node errors and update node graph ([#11060](https://github.com/n8n-io/n8n/issues/11060)) ([d3b05f1](https://github.com/n8n-io/n8n/commit/d3b05f1c54e62440666297d8e484ccd22168da48))
|
||||
* **core:** Dedupe ([#10101](https://github.com/n8n-io/n8n/issues/10101)) ([52dd2c7](https://github.com/n8n-io/n8n/commit/52dd2c76196c6895b47145c2b85a6895ce2874d4))
|
||||
* **editor:** Send workflow context to assistant store ([#11135](https://github.com/n8n-io/n8n/issues/11135)) ([fade9e4](https://github.com/n8n-io/n8n/commit/fade9e43c84a0ae1fbc80f3ee546a418970e2380))
|
||||
* **Gong Node:** New node ([#10777](https://github.com/n8n-io/n8n/issues/10777)) ([785b47f](https://github.com/n8n-io/n8n/commit/785b47feb3b83cf36aaed57123f8baca2bbab307))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **Google Sheets Node:** Don't load whole spreadsheet dataset to determine columns when appending data ([#11235](https://github.com/n8n-io/n8n/issues/11235)) ([26ad091](https://github.com/n8n-io/n8n/commit/26ad091f473bca4e5d3bdc257e0818be02e52db5))
|
||||
|
||||
|
||||
|
||||
# [1.63.0](https://github.com/n8n-io/n8n/compare/n8n@1.62.1...n8n@1.63.0) (2024-10-09)
|
||||
|
||||
|
||||
|
|
|
@ -68,7 +68,7 @@ If you already have VS Code and Docker installed, you can click [here](https://v
|
|||
|
||||
#### Node.js
|
||||
|
||||
[Node.js](https://nodejs.org/en/) version 18.10 or newer is required for development purposes.
|
||||
[Node.js](https://nodejs.org/en/) version 20.15 or newer is required for development purposes.
|
||||
|
||||
#### pnpm
|
||||
|
||||
|
|
|
@ -40,6 +40,14 @@ export function getOutputPanelDataContainer() {
|
|||
return getOutputPanel().getByTestId('ndv-data-container');
|
||||
}
|
||||
|
||||
export function getOutputTableRows() {
|
||||
return getOutputPanelDataContainer().find('table tr');
|
||||
}
|
||||
|
||||
export function getOutputTableRow(row: number) {
|
||||
return getOutputTableRows().eq(row);
|
||||
}
|
||||
|
||||
export function getOutputPanelTable() {
|
||||
return getOutputPanelDataContainer().get('table');
|
||||
}
|
||||
|
|
|
@ -69,6 +69,13 @@ export function getNodeCreatorPlusButton() {
|
|||
return cy.getByTestId('node-creator-plus-button');
|
||||
}
|
||||
|
||||
export function getCanvasNodes() {
|
||||
return cy.ifCanvasVersion(
|
||||
() => cy.getByTestId('canvas-node'),
|
||||
() => cy.getByTestId('canvas-node').not('[data-node-type="n8n-nodes-internal.addNodes"]'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions
|
||||
*/
|
||||
|
|
|
@ -20,6 +20,7 @@ describe('Undo/Redo', () => {
|
|||
WorkflowPage.actions.visit();
|
||||
});
|
||||
|
||||
// FIXME: Canvas V2: Fix redo connections
|
||||
it('should undo/redo adding node in the middle', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
|
@ -114,6 +115,7 @@ describe('Undo/Redo', () => {
|
|||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
||||
});
|
||||
|
||||
// FIXME: Canvas V2: Fix moving of nodes via e2e tests
|
||||
it('should undo/redo moving nodes', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
|
@ -146,18 +148,14 @@ describe('Undo/Redo', () => {
|
|||
it('should undo/redo deleting a connection using context menu', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
WorkflowPage.getters.nodeConnections().realHover();
|
||||
cy.get('.connection-actions .delete')
|
||||
.filter(':visible')
|
||||
.should('be.visible')
|
||||
.click({ force: true });
|
||||
WorkflowPage.actions.deleteNodeBetweenNodes(SCHEDULE_TRIGGER_NODE_NAME, CODE_NODE_NAME);
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
||||
WorkflowPage.actions.hitUndo();
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
||||
WorkflowPage.actions.hitRedo();
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
||||
});
|
||||
|
||||
// FIXME: Canvas V2: Fix disconnecting by moving
|
||||
it('should undo/redo deleting a connection by moving it away', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
|
@ -206,6 +204,7 @@ describe('Undo/Redo', () => {
|
|||
WorkflowPage.getters.disabledNodes().should('have.length', 2);
|
||||
});
|
||||
|
||||
// FIXME: Canvas V2: Fix undo renaming node
|
||||
it('should undo/redo renaming node using keyboard shortcut', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
|
@ -244,6 +243,7 @@ describe('Undo/Redo', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// FIXME: Canvas V2: Figure out why moving doesn't work from e2e
|
||||
it('should undo/redo multiple steps', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
|
|
|
@ -16,6 +16,7 @@ describe('Canvas Actions', () => {
|
|||
WorkflowPage.actions.visit();
|
||||
});
|
||||
|
||||
// FIXME: Canvas V2: Missing execute button if no nodes
|
||||
it('should render canvas', () => {
|
||||
WorkflowPage.getters.nodeViewRoot().should('be.visible');
|
||||
WorkflowPage.getters.canvasPlusButton().should('be.visible');
|
||||
|
@ -25,10 +26,11 @@ describe('Canvas Actions', () => {
|
|||
WorkflowPage.getters.executeWorkflowButton().should('be.visible');
|
||||
});
|
||||
|
||||
// FIXME: Canvas V2: Fix changing of connection
|
||||
it('should connect and disconnect a simple node', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
|
||||
WorkflowPage.getters.nodeViewBackground().click(600, 200, { force: true });
|
||||
cy.get('.jtk-connector').should('have.length', 1);
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
||||
|
||||
WorkflowPage.getters.nodeViewBackground().click(600, 400, { force: true });
|
||||
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
|
||||
|
@ -40,16 +42,16 @@ describe('Canvas Actions', () => {
|
|||
);
|
||||
|
||||
WorkflowPage.getters
|
||||
.canvasNodeInputEndpointByName(`${EDIT_FIELDS_SET_NODE_NAME}1`)
|
||||
.should('have.class', 'jtk-endpoint-connected');
|
||||
.getConnectionBetweenNodes(MANUAL_TRIGGER_NODE_DISPLAY_NAME, `${EDIT_FIELDS_SET_NODE_NAME}1`)
|
||||
.should('be.visible');
|
||||
|
||||
cy.get('.jtk-connector').should('have.length', 1);
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
||||
// Disconnect Set1
|
||||
cy.drag(
|
||||
WorkflowPage.getters.getEndpointSelector('input', `${EDIT_FIELDS_SET_NODE_NAME}1`),
|
||||
[-200, 100],
|
||||
);
|
||||
cy.get('.jtk-connector').should('have.length', 0);
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
||||
});
|
||||
|
||||
it('should add first step', () => {
|
||||
|
@ -74,7 +76,7 @@ describe('Canvas Actions', () => {
|
|||
|
||||
it('should add a connected node using plus endpoint', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
cy.get('.plus-endpoint').should('be.visible').click();
|
||||
WorkflowPage.getters.canvasNodePlusEndpointByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
|
||||
WorkflowPage.getters.nodeCreatorSearchBar().type(CODE_NODE_NAME);
|
||||
WorkflowPage.getters.nodeCreatorSearchBar().type('{enter}');
|
||||
|
@ -85,7 +87,7 @@ describe('Canvas Actions', () => {
|
|||
|
||||
it('should add a connected node dragging from node creator', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
cy.get('.plus-endpoint').should('be.visible').click();
|
||||
WorkflowPage.getters.canvasNodePlusEndpointByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
|
||||
WorkflowPage.getters.nodeCreatorSearchBar().type(CODE_NODE_NAME);
|
||||
cy.drag(WorkflowPage.getters.nodeCreatorNodeItems().first(), [100, 100], {
|
||||
|
@ -99,7 +101,7 @@ describe('Canvas Actions', () => {
|
|||
|
||||
it('should open a category when trying to drag and drop it on the canvas', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
cy.get('.plus-endpoint').should('be.visible').click();
|
||||
WorkflowPage.getters.canvasNodePlusEndpointByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
|
||||
WorkflowPage.getters.nodeCreatorSearchBar().type(CODE_NODE_NAME);
|
||||
cy.drag(WorkflowPage.getters.nodeCreatorActionItems().first(), [100, 100], {
|
||||
|
@ -114,7 +116,7 @@ describe('Canvas Actions', () => {
|
|||
it('should add disconnected node if nothing is selected', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
// Deselect nodes
|
||||
WorkflowPage.getters.nodeViewBackground().click({ force: true });
|
||||
WorkflowPage.getters.nodeView().click({ force: true });
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
WorkflowPage.getters.canvasNodes().should('have.length', 2);
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
||||
|
@ -136,10 +138,10 @@ describe('Canvas Actions', () => {
|
|||
WorkflowPage.getters.nodeConnections().should('have.length', 3);
|
||||
|
||||
WorkflowPage.getters.canvasNodeByName(EDIT_FIELDS_SET_NODE_NAME).then(($editFieldsNode) => {
|
||||
const editFieldsNodeLeft = parseFloat($editFieldsNode.css('left'));
|
||||
const editFieldsNodeLeft = WorkflowPage.getters.getNodeLeftPosition($editFieldsNode);
|
||||
|
||||
WorkflowPage.getters.canvasNodeByName(HTTP_REQUEST_NODE_NAME).then(($httpNode) => {
|
||||
const httpNodeLeft = parseFloat($httpNode.css('left'));
|
||||
const httpNodeLeft = WorkflowPage.getters.getNodeLeftPosition($httpNode);
|
||||
expect(httpNodeLeft).to.be.lessThan(editFieldsNodeLeft);
|
||||
});
|
||||
});
|
||||
|
@ -159,10 +161,12 @@ describe('Canvas Actions', () => {
|
|||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
WorkflowPage.getters.nodeConnections().first().realHover();
|
||||
cy.get('.connection-actions .delete').first().click({ force: true });
|
||||
WorkflowPage.actions.deleteNodeBetweenNodes(MANUAL_TRIGGER_NODE_DISPLAY_NAME, CODE_NODE_NAME);
|
||||
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
||||
});
|
||||
|
||||
// FIXME: Canvas V2: Fix disconnecting of connection by dragging it
|
||||
it('should delete a connection by moving it away from endpoint', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||
|
@ -216,10 +220,10 @@ describe('Canvas Actions', () => {
|
|||
WorkflowPage.actions.hitSelectAll();
|
||||
|
||||
WorkflowPage.actions.hitCopy();
|
||||
successToast().should('contain', 'Copied!');
|
||||
successToast().should('contain', 'Copied to clipboard');
|
||||
|
||||
WorkflowPage.actions.copyNode(CODE_NODE_NAME);
|
||||
successToast().should('contain', 'Copied!');
|
||||
successToast().should('contain', 'Copied to clipboard');
|
||||
});
|
||||
|
||||
it('should select/deselect all nodes', () => {
|
||||
|
@ -231,17 +235,31 @@ describe('Canvas Actions', () => {
|
|||
WorkflowPage.getters.selectedNodes().should('have.length', 0);
|
||||
});
|
||||
|
||||
// FIXME: Canvas V2: Selection via arrow keys is broken
|
||||
it('should select nodes using arrow keys', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
cy.wait(500);
|
||||
cy.get('body').type('{leftArrow}');
|
||||
WorkflowPage.getters.canvasNodes().first().should('have.class', 'jtk-drag-selected');
|
||||
const selectedCanvasNodes = () =>
|
||||
cy.ifCanvasVersion(
|
||||
() => WorkflowPage.getters.canvasNodes(),
|
||||
() => WorkflowPage.getters.canvasNodes().parent(),
|
||||
);
|
||||
|
||||
cy.ifCanvasVersion(
|
||||
() => selectedCanvasNodes().first().should('have.class', 'jtk-drag-selected'),
|
||||
() => selectedCanvasNodes().first().should('have.class', 'selected'),
|
||||
);
|
||||
cy.get('body').type('{rightArrow}');
|
||||
WorkflowPage.getters.canvasNodes().last().should('have.class', 'jtk-drag-selected');
|
||||
cy.ifCanvasVersion(
|
||||
() => selectedCanvasNodes().last().should('have.class', 'jtk-drag-selected'),
|
||||
() => selectedCanvasNodes().last().should('have.class', 'selected'),
|
||||
);
|
||||
});
|
||||
|
||||
// FIXME: Canvas V2: Selection via shift and arrow keys is broken
|
||||
it('should select nodes using shift and arrow keys', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||
|
@ -251,6 +269,7 @@ describe('Canvas Actions', () => {
|
|||
WorkflowPage.getters.selectedNodes().should('have.length', 2);
|
||||
});
|
||||
|
||||
// FIXME: Canvas V2: Fix select & deselect
|
||||
it('should not break lasso selection when dragging node action buttons', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.getters
|
||||
|
@ -262,6 +281,7 @@ describe('Canvas Actions', () => {
|
|||
WorkflowPage.actions.testLassoSelection([100, 100], [200, 200]);
|
||||
});
|
||||
|
||||
// FIXME: Canvas V2: Fix select & deselect
|
||||
it('should not break lasso selection with multiple clicks on node action buttons', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.testLassoSelection([100, 100], [200, 200]);
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
} from './../constants';
|
||||
import { NDV, WorkflowExecutionsTab } from '../pages';
|
||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
import { isCanvasV2 } from '../utils/workflowUtils';
|
||||
|
||||
const WorkflowPage = new WorkflowPageClass();
|
||||
const ExecutionsTab = new WorkflowExecutionsTab();
|
||||
|
@ -52,15 +53,15 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
cy.reload();
|
||||
cy.waitForLoad();
|
||||
// Make sure outputless switch was connected correctly
|
||||
cy.get(
|
||||
`[data-target-node="${SWITCH_NODE_NAME}1"][data-source-node="${EDIT_FIELDS_SET_NODE_NAME}3"]`,
|
||||
).should('be.visible');
|
||||
WorkflowPage.getters
|
||||
.getConnectionBetweenNodes(`${EDIT_FIELDS_SET_NODE_NAME}3`, `${SWITCH_NODE_NAME}1`)
|
||||
.should('exist');
|
||||
// Make sure all connections are there after reload
|
||||
for (let i = 0; i < desiredOutputs; i++) {
|
||||
const setName = `${EDIT_FIELDS_SET_NODE_NAME}${i > 0 ? i : ''}`;
|
||||
WorkflowPage.getters
|
||||
.canvasNodeInputEndpointByName(setName)
|
||||
.should('have.class', 'jtk-endpoint-connected');
|
||||
.getConnectionBetweenNodes(`${SWITCH_NODE_NAME}`, setName)
|
||||
.should('exist');
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -69,9 +70,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||
for (let i = 0; i < 2; i++) {
|
||||
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true);
|
||||
WorkflowPage.getters
|
||||
.nodeViewBackground()
|
||||
.click((i + 1) * 200, (i + 1) * 200, { force: true });
|
||||
WorkflowPage.getters.nodeView().click((i + 1) * 200, (i + 1) * 200, { force: true });
|
||||
}
|
||||
WorkflowPage.actions.zoomToFit();
|
||||
|
||||
|
@ -84,8 +83,6 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
WorkflowPage.getters.getEndpointSelector('input', `${EDIT_FIELDS_SET_NODE_NAME}1`),
|
||||
);
|
||||
|
||||
cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 2);
|
||||
|
||||
// Connect Set1 and Set2 to merge
|
||||
cy.draganddrop(
|
||||
WorkflowPage.getters.getEndpointSelector('plus', EDIT_FIELDS_SET_NODE_NAME),
|
||||
|
@ -95,20 +92,36 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
WorkflowPage.getters.getEndpointSelector('plus', `${EDIT_FIELDS_SET_NODE_NAME}1`),
|
||||
WorkflowPage.getters.getEndpointSelector('input', MERGE_NODE_NAME, 1),
|
||||
);
|
||||
|
||||
cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 4);
|
||||
const checkConnections = () => {
|
||||
WorkflowPage.getters
|
||||
.getConnectionBetweenNodes(
|
||||
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
|
||||
`${EDIT_FIELDS_SET_NODE_NAME}1`,
|
||||
)
|
||||
.should('exist');
|
||||
WorkflowPage.getters
|
||||
.getConnectionBetweenNodes(EDIT_FIELDS_SET_NODE_NAME, MERGE_NODE_NAME)
|
||||
.should('exist');
|
||||
WorkflowPage.getters
|
||||
.getConnectionBetweenNodes(`${EDIT_FIELDS_SET_NODE_NAME}1`, MERGE_NODE_NAME)
|
||||
.should('exist');
|
||||
};
|
||||
checkConnections();
|
||||
|
||||
// Make sure all connections are there after save & reload
|
||||
WorkflowPage.actions.saveWorkflowOnButtonClick();
|
||||
cy.reload();
|
||||
cy.waitForLoad();
|
||||
|
||||
cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 4);
|
||||
checkConnections();
|
||||
// cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 4);
|
||||
WorkflowPage.actions.executeWorkflow();
|
||||
WorkflowPage.getters.stopExecutionButton().should('not.exist');
|
||||
|
||||
// If the merged set nodes are connected and executed correctly, there should be 2 items in the output of merge node
|
||||
cy.get('[data-label="2 items"]').should('be.visible');
|
||||
cy.ifCanvasVersion(
|
||||
() => cy.get('[data-label="2 items"]').should('be.visible'),
|
||||
() => cy.getByTestId('canvas-node-output-handle').contains('2 items').should('be.visible'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should add nodes and check execution success', () => {
|
||||
|
@ -120,16 +133,42 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
WorkflowPage.actions.zoomToFit();
|
||||
WorkflowPage.actions.executeWorkflow();
|
||||
|
||||
cy.get('.jtk-connector.success').should('have.length', 3);
|
||||
cy.get('.data-count').should('have.length', 4);
|
||||
cy.get('.plus-draggable-endpoint').should('have.class', 'ep-success');
|
||||
cy.ifCanvasVersion(
|
||||
() => cy.get('.jtk-connector.success').should('have.length', 3),
|
||||
() => cy.get('[data-edge-status=success]').should('have.length', 3),
|
||||
);
|
||||
cy.ifCanvasVersion(
|
||||
() => cy.get('.data-count').should('have.length', 4),
|
||||
() => cy.getByTestId('canvas-node-status-success').should('have.length', 4),
|
||||
);
|
||||
|
||||
cy.ifCanvasVersion(
|
||||
() => cy.get('.plus-draggable-endpoint').should('have.class', 'ep-success'),
|
||||
() => cy.getByTestId('canvas-handle-plus').should('have.attr', 'data-plus-type', 'success'),
|
||||
);
|
||||
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
WorkflowPage.actions.zoomToFit();
|
||||
|
||||
cy.get('.plus-draggable-endpoint').filter(':visible').should('not.have.class', 'ep-success');
|
||||
cy.get('.jtk-connector.success').should('have.length', 3);
|
||||
cy.get('.jtk-connector').should('have.length', 4);
|
||||
cy.ifCanvasVersion(
|
||||
() =>
|
||||
cy
|
||||
.get('.plus-draggable-endpoint')
|
||||
.filter(':visible')
|
||||
.should('not.have.class', 'ep-success'),
|
||||
() =>
|
||||
cy.getByTestId('canvas-handle-plus').should('not.have.attr', 'data-plus-type', 'success'),
|
||||
);
|
||||
|
||||
cy.ifCanvasVersion(
|
||||
() => cy.get('.jtk-connector.success').should('have.length', 3),
|
||||
// The new version of the canvas correctly shows executed data being passed to the input of the next node
|
||||
() => cy.get('[data-edge-status=success]').should('have.length', 4),
|
||||
);
|
||||
cy.ifCanvasVersion(
|
||||
() => cy.get('.data-count').should('have.length', 4),
|
||||
() => cy.getByTestId('canvas-node-status-success').should('have.length', 4),
|
||||
);
|
||||
});
|
||||
|
||||
it('should delete node using context menu', () => {
|
||||
|
@ -194,19 +233,29 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
WorkflowPage.getters.canvasNodes().should('have.length', 0);
|
||||
});
|
||||
|
||||
// FIXME: Canvas V2: Figure out how to test moving of the node
|
||||
it('should move node', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
|
||||
WorkflowPage.actions.zoomToFit();
|
||||
WorkflowPage.getters
|
||||
.canvasNodes()
|
||||
.last()
|
||||
.then(($node) => {
|
||||
const { left, top } = $node.position();
|
||||
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], {
|
||||
clickToFinish: true,
|
||||
});
|
||||
|
||||
if (isCanvasV2()) {
|
||||
cy.drag('.vue-flow__node', [300, 300], {
|
||||
realMouse: true,
|
||||
});
|
||||
} else {
|
||||
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], {
|
||||
clickToFinish: true,
|
||||
});
|
||||
}
|
||||
|
||||
WorkflowPage.getters
|
||||
.canvasNodes()
|
||||
.last()
|
||||
|
@ -218,91 +267,80 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should zoom in', () => {
|
||||
WorkflowPage.getters.zoomInButton().should('be.visible').click();
|
||||
WorkflowPage.getters
|
||||
.nodeView()
|
||||
.should(
|
||||
'have.css',
|
||||
'transform',
|
||||
`matrix(${ZOOM_IN_X1_FACTOR}, 0, 0, ${ZOOM_IN_X1_FACTOR}, 0, 0)`,
|
||||
describe('Canvas Zoom Functionality', () => {
|
||||
const getContainer = () =>
|
||||
cy.ifCanvasVersion(
|
||||
() => WorkflowPage.getters.nodeView(),
|
||||
() => WorkflowPage.getters.canvasViewport(),
|
||||
);
|
||||
WorkflowPage.getters.zoomInButton().click();
|
||||
WorkflowPage.getters
|
||||
.nodeView()
|
||||
.should(
|
||||
'have.css',
|
||||
'transform',
|
||||
`matrix(${ZOOM_IN_X2_FACTOR}, 0, 0, ${ZOOM_IN_X2_FACTOR}, 0, 0)`,
|
||||
);
|
||||
});
|
||||
const checkZoomLevel = (expectedFactor: number) => {
|
||||
return getContainer().should(($nodeView) => {
|
||||
const newTransform = $nodeView.css('transform');
|
||||
const newScale = parseFloat(newTransform.split(',')[0].slice(7));
|
||||
|
||||
it('should zoom out', () => {
|
||||
WorkflowPage.getters.zoomOutButton().should('be.visible').click();
|
||||
WorkflowPage.getters
|
||||
.nodeView()
|
||||
.should(
|
||||
'have.css',
|
||||
'transform',
|
||||
`matrix(${ZOOM_OUT_X1_FACTOR}, 0, 0, ${ZOOM_OUT_X1_FACTOR}, 0, 0)`,
|
||||
);
|
||||
WorkflowPage.getters.zoomOutButton().click();
|
||||
WorkflowPage.getters
|
||||
.nodeView()
|
||||
.should(
|
||||
'have.css',
|
||||
'transform',
|
||||
`matrix(${ZOOM_OUT_X2_FACTOR}, 0, 0, ${ZOOM_OUT_X2_FACTOR}, 0, 0)`,
|
||||
);
|
||||
});
|
||||
expect(newScale).to.be.closeTo(expectedFactor, 0.2);
|
||||
});
|
||||
};
|
||||
|
||||
it('should zoom using scroll or pinch gesture', () => {
|
||||
WorkflowPage.actions.pinchToZoom(1, 'zoomIn');
|
||||
WorkflowPage.getters
|
||||
.nodeView()
|
||||
.should(
|
||||
'have.css',
|
||||
'transform',
|
||||
`matrix(${PINCH_ZOOM_IN_FACTOR}, 0, 0, ${PINCH_ZOOM_IN_FACTOR}, 0, 0)`,
|
||||
const zoomAndCheck = (action: 'zoomIn' | 'zoomOut', expectedFactor: number) => {
|
||||
WorkflowPage.getters[`${action}Button`]().click();
|
||||
checkZoomLevel(expectedFactor);
|
||||
};
|
||||
|
||||
it('should zoom in', () => {
|
||||
WorkflowPage.getters.zoomInButton().should('be.visible');
|
||||
getContainer().then(($nodeView) => {
|
||||
const initialTransform = $nodeView.css('transform');
|
||||
const initialScale =
|
||||
initialTransform === 'none' ? 1 : parseFloat(initialTransform.split(',')[0].slice(7));
|
||||
|
||||
zoomAndCheck('zoomIn', initialScale * ZOOM_IN_X1_FACTOR);
|
||||
zoomAndCheck('zoomIn', initialScale * ZOOM_IN_X2_FACTOR);
|
||||
});
|
||||
});
|
||||
|
||||
it('should zoom out', () => {
|
||||
zoomAndCheck('zoomOut', ZOOM_OUT_X1_FACTOR);
|
||||
zoomAndCheck('zoomOut', ZOOM_OUT_X2_FACTOR);
|
||||
});
|
||||
|
||||
it('should zoom using scroll or pinch gesture', () => {
|
||||
WorkflowPage.actions.pinchToZoom(1, 'zoomIn');
|
||||
|
||||
// V2 Canvas is using the same zoom factor for both pinch and scroll
|
||||
cy.ifCanvasVersion(
|
||||
() => checkZoomLevel(PINCH_ZOOM_IN_FACTOR),
|
||||
() => checkZoomLevel(ZOOM_IN_X1_FACTOR),
|
||||
);
|
||||
|
||||
WorkflowPage.actions.pinchToZoom(1, 'zoomOut');
|
||||
// Zoom in 1x + Zoom out 1x should reset to default (=1)
|
||||
WorkflowPage.getters.nodeView().should('have.css', 'transform', 'matrix(1, 0, 0, 1, 0, 0)');
|
||||
WorkflowPage.actions.pinchToZoom(1, 'zoomOut');
|
||||
checkZoomLevel(1); // Zoom in 1x + Zoom out 1x should reset to default (=1)
|
||||
|
||||
WorkflowPage.actions.pinchToZoom(1, 'zoomOut');
|
||||
WorkflowPage.getters
|
||||
.nodeView()
|
||||
.should(
|
||||
'have.css',
|
||||
'transform',
|
||||
`matrix(${PINCH_ZOOM_OUT_FACTOR}, 0, 0, ${PINCH_ZOOM_OUT_FACTOR}, 0, 0)`,
|
||||
WorkflowPage.actions.pinchToZoom(1, 'zoomOut');
|
||||
|
||||
cy.ifCanvasVersion(
|
||||
() => checkZoomLevel(PINCH_ZOOM_OUT_FACTOR),
|
||||
() => checkZoomLevel(ZOOM_OUT_X1_FACTOR),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should reset zoom', () => {
|
||||
// Reset zoom should not appear until zoom level changed
|
||||
WorkflowPage.getters.resetZoomButton().should('not.exist');
|
||||
WorkflowPage.getters.zoomInButton().click();
|
||||
WorkflowPage.getters.resetZoomButton().should('be.visible').click();
|
||||
WorkflowPage.getters
|
||||
.nodeView()
|
||||
.should(
|
||||
'have.css',
|
||||
'transform',
|
||||
`matrix(${DEFAULT_ZOOM_FACTOR}, 0, 0, ${DEFAULT_ZOOM_FACTOR}, 0, 0)`,
|
||||
);
|
||||
});
|
||||
it('should reset zoom', () => {
|
||||
WorkflowPage.getters.resetZoomButton().should('not.exist');
|
||||
WorkflowPage.getters.zoomInButton().click();
|
||||
WorkflowPage.getters.resetZoomButton().should('be.visible').click();
|
||||
checkZoomLevel(DEFAULT_ZOOM_FACTOR);
|
||||
});
|
||||
|
||||
it('should zoom to fit', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
// At this point last added node should be off-screen
|
||||
WorkflowPage.getters.canvasNodes().last().should('not.be.visible');
|
||||
WorkflowPage.getters.zoomToFitButton().click();
|
||||
WorkflowPage.getters.canvasNodes().last().should('be.visible');
|
||||
it('should zoom to fit', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
// At this point last added node should be off-screen
|
||||
WorkflowPage.getters.canvasNodes().last().should('not.be.visible');
|
||||
WorkflowPage.getters.zoomToFitButton().click();
|
||||
WorkflowPage.getters.canvasNodes().last().should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
it('should disable node (context menu or shortcut)', () => {
|
||||
|
@ -426,9 +464,9 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
cy.reload();
|
||||
cy.waitForLoad();
|
||||
WorkflowPage.getters.canvasNodes().should('have.length', 2);
|
||||
cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 1);
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
||||
});
|
||||
|
||||
// FIXME: Canvas V2: Credentials should show issue on the first open
|
||||
it('should remove unknown credentials on pasting workflow', () => {
|
||||
cy.fixture('workflow-with-unknown-credentials.json').then((data) => {
|
||||
cy.get('body').paste(JSON.stringify(data));
|
||||
|
@ -441,6 +479,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// FIXME: Canvas V2: Unknown nodes should still render connection endpoints
|
||||
it('should render connections correctly if unkown nodes are present', () => {
|
||||
const unknownNodeName = 'Unknown node';
|
||||
cy.createFixtureWorkflow('workflow-with-unknown-nodes.json', 'Unknown nodes');
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { simpleWebhookCall, waitForWebhook } from './16-webhook-node.cy';
|
||||
import {
|
||||
HTTP_REQUEST_NODE_NAME,
|
||||
MANUAL_TRIGGER_NODE_NAME,
|
||||
|
@ -7,6 +10,7 @@ import {
|
|||
} from '../constants';
|
||||
import { WorkflowPage, NDV } from '../pages';
|
||||
import { errorToast } from '../pages/notifications';
|
||||
import { getVisiblePopper } from '../utils';
|
||||
|
||||
const workflowPage = new WorkflowPage();
|
||||
const ndv = new NDV();
|
||||
|
@ -212,6 +216,42 @@ describe('Data pinning', () => {
|
|||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should show pinned data tooltip', () => {
|
||||
const { callEndpoint } = simpleWebhookCall({
|
||||
method: 'GET',
|
||||
webhookPath: nanoid(),
|
||||
executeNow: false,
|
||||
});
|
||||
|
||||
ndv.actions.close();
|
||||
workflowPage.actions.executeWorkflow();
|
||||
cy.wait(waitForWebhook);
|
||||
|
||||
// hide other visible popper on workflow execute button
|
||||
workflowPage.getters.canvasNodes().eq(0).click();
|
||||
|
||||
callEndpoint((response) => {
|
||||
expect(response.status).to.eq(200);
|
||||
getVisiblePopper().should('have.length', 1);
|
||||
getVisiblePopper()
|
||||
.eq(0)
|
||||
.should(
|
||||
'have.text',
|
||||
'You can pin this output instead of waiting for a test event. Open node to do so.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not show pinned data tooltip', () => {
|
||||
cy.createFixtureWorkflow('Pinned_webhook_node.json', 'Test');
|
||||
workflowPage.actions.executeWorkflow();
|
||||
|
||||
// hide other visible popper on workflow execute button
|
||||
workflowPage.getters.canvasNodes().eq(0).click();
|
||||
|
||||
getVisiblePopper().should('have.length', 0);
|
||||
});
|
||||
});
|
||||
|
||||
function setExpressionOnStringValueInSet(expression: string) {
|
||||
|
|
|
@ -16,12 +16,14 @@ describe('n8n Form Trigger', () => {
|
|||
ndv.getters.parameterInput('formDescription').type('Test Form Description');
|
||||
ndv.getters.parameterInput('fieldLabel').type('Test Field 1');
|
||||
ndv.getters.backToCanvas().click();
|
||||
workflowPage.getters.nodeIssuesByName('n8n Form Trigger').should('not.exist');
|
||||
workflowPage.getters.nodeIssuesByName('On form submission').should('not.exist');
|
||||
});
|
||||
|
||||
it('should fill up form fields', () => {
|
||||
workflowPage.actions.addInitialNodeToCanvas('n8n Form Trigger');
|
||||
workflowPage.getters.canvasNodes().first().dblclick();
|
||||
workflowPage.actions.addInitialNodeToCanvas('n8n Form Trigger', {
|
||||
isTrigger: true,
|
||||
action: 'On new n8n Form event',
|
||||
});
|
||||
ndv.getters.parameterInput('formTitle').type('Test Form');
|
||||
ndv.getters.parameterInput('formDescription').type('Test Form Description');
|
||||
//fill up first field of type number
|
||||
|
@ -96,6 +98,6 @@ describe('n8n Form Trigger', () => {
|
|||
.type('Your test form was successfully submitted');
|
||||
|
||||
ndv.getters.backToCanvas().click();
|
||||
workflowPage.getters.nodeIssuesByName('n8n Form Trigger').should('not.exist');
|
||||
workflowPage.getters.nodeIssuesByName('On form submission').should('not.exist');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,7 +9,7 @@ const workflowPage = new WorkflowPage();
|
|||
const ndv = new NDV();
|
||||
const credentialsModal = new CredentialsModal();
|
||||
|
||||
const waitForWebhook = 500;
|
||||
export const waitForWebhook = 500;
|
||||
|
||||
interface SimpleWebhookCallOptions {
|
||||
method: string;
|
||||
|
@ -21,7 +21,7 @@ interface SimpleWebhookCallOptions {
|
|||
authentication?: string;
|
||||
}
|
||||
|
||||
const simpleWebhookCall = (options: SimpleWebhookCallOptions) => {
|
||||
export const simpleWebhookCall = (options: SimpleWebhookCallOptions) => {
|
||||
const {
|
||||
authentication,
|
||||
method,
|
||||
|
@ -65,15 +65,23 @@ const simpleWebhookCall = (options: SimpleWebhookCallOptions) => {
|
|||
getVisibleSelect().find('.option-headline').contains(responseData).click();
|
||||
}
|
||||
|
||||
const callEndpoint = (cb: (response: Cypress.Response<unknown>) => void) => {
|
||||
cy.request(method, `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then(cb);
|
||||
};
|
||||
|
||||
if (executeNow) {
|
||||
ndv.actions.execute();
|
||||
cy.wait(waitForWebhook);
|
||||
|
||||
cy.request(method, `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then((response) => {
|
||||
callEndpoint((response) => {
|
||||
expect(response.status).to.eq(200);
|
||||
ndv.getters.outputPanel().contains('headers');
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
callEndpoint,
|
||||
};
|
||||
};
|
||||
|
||||
describe('Webhook Trigger node', () => {
|
||||
|
|
|
@ -226,6 +226,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
|
|||
.should('have.length', 1)
|
||||
.click();
|
||||
credentialsModal.getters.saveButton().click();
|
||||
credentialsModal.getters.saveButton().should('have.text', 'Saved');
|
||||
credentialsModal.actions.close();
|
||||
|
||||
projects.getProjectTabWorkflows().click();
|
||||
|
@ -252,12 +253,13 @@ describe('Sharing', { disableAutoLogin: true }, () => {
|
|||
credentialsModal.getters.usersSelect().click();
|
||||
getVisibleSelect().find('li').should('have.length', 4).first().click();
|
||||
credentialsModal.getters.saveButton().click();
|
||||
credentialsModal.getters.saveButton().should('have.text', 'Saved');
|
||||
credentialsModal.actions.close();
|
||||
|
||||
credentialsPage.getters
|
||||
.credentialCards()
|
||||
.should('have.length', 2)
|
||||
.filter(':contains("Owned by me")')
|
||||
.filter(':contains("Personal")')
|
||||
.should('have.length', 1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { SCHEDULE_TRIGGER_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME } from '../constants';
|
||||
import { NDV, WorkflowExecutionsTab, WorkflowPage as WorkflowPageClass } from '../pages';
|
||||
import { clearNotifications, errorToast, successToast } from '../pages/notifications';
|
||||
import { isCanvasV2 } from '../utils/workflowUtils';
|
||||
|
||||
const workflowPage = new WorkflowPageClass();
|
||||
const executionsTab = new WorkflowExecutionsTab();
|
||||
|
@ -117,15 +118,22 @@ describe('Execution', () => {
|
|||
.canvasNodeByName('Manual')
|
||||
.within(() => cy.get('.fa-check'))
|
||||
.should('exist');
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Wait')
|
||||
.within(() => cy.get('.fa-sync-alt').should('not.visible'));
|
||||
|
||||
if (isCanvasV2()) {
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Wait')
|
||||
.within(() => cy.get('.fa-sync-alt').should('not.exist'));
|
||||
} else {
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Wait')
|
||||
.within(() => cy.get('.fa-sync-alt').should('not.be.visible'));
|
||||
}
|
||||
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Set')
|
||||
.within(() => cy.get('.fa-check').should('not.exist'));
|
||||
|
||||
successToast().should('be.visible');
|
||||
clearNotifications();
|
||||
|
||||
// Clear execution data
|
||||
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
||||
|
@ -206,6 +214,7 @@ describe('Execution', () => {
|
|||
workflowPage.getters.clearExecutionDataButton().should('not.exist');
|
||||
});
|
||||
|
||||
// FIXME: Canvas V2: Webhook should show waiting state but it doesn't
|
||||
it('should test webhook workflow stop', () => {
|
||||
cy.createFixtureWorkflow('Webhook_wait_set.json');
|
||||
|
||||
|
@ -267,9 +276,17 @@ describe('Execution', () => {
|
|||
.canvasNodeByName('Webhook')
|
||||
.within(() => cy.get('.fa-check'))
|
||||
.should('exist');
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Wait')
|
||||
.within(() => cy.get('.fa-sync-alt').should('not.visible'));
|
||||
|
||||
if (isCanvasV2()) {
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Wait')
|
||||
.within(() => cy.get('.fa-sync-alt').should('not.exist'));
|
||||
} else {
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Wait')
|
||||
.within(() => cy.get('.fa-sync-alt').should('not.be.visible'));
|
||||
}
|
||||
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Set')
|
||||
.within(() => cy.get('.fa-check').should('not.exist'));
|
||||
|
@ -295,6 +312,7 @@ describe('Execution', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// FIXME: Canvas V2: Missing pinned states for `edge-label-wrapper`
|
||||
describe('connections should be colored differently for pinned data', () => {
|
||||
beforeEach(() => {
|
||||
cy.createFixtureWorkflow('Schedule_pinned.json');
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
TRELLO_NODE_NAME,
|
||||
} from '../constants';
|
||||
import { CredentialsModal, CredentialsPage, NDV, WorkflowPage } from '../pages';
|
||||
import { successToast } from '../pages/notifications';
|
||||
import { errorToast, successToast } from '../pages/notifications';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
|
||||
const credentialsPage = new CredentialsPage();
|
||||
|
@ -26,6 +26,22 @@ const nodeDetailsView = new NDV();
|
|||
const NEW_CREDENTIAL_NAME = 'Something else';
|
||||
const NEW_CREDENTIAL_NAME2 = 'Something else entirely';
|
||||
|
||||
function createNotionCredential() {
|
||||
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME);
|
||||
workflowPage.actions.openNode(NOTION_NODE_NAME);
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
getVisibleSelect().find('li').last().click();
|
||||
credentialsModal.actions.fillCredentialsForm();
|
||||
cy.get('body').type('{esc}');
|
||||
workflowPage.actions.deleteNode(NOTION_NODE_NAME);
|
||||
}
|
||||
|
||||
function deleteSelectedCredential() {
|
||||
workflowPage.getters.nodeCredentialsEditButton().click();
|
||||
credentialsModal.getters.deleteButton().click();
|
||||
cy.get('.el-message-box').find('button').contains('Yes').click();
|
||||
}
|
||||
|
||||
describe('Credentials', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit(credentialsPage.url);
|
||||
|
@ -229,6 +245,40 @@ describe('Credentials', () => {
|
|||
.should('have.value', NEW_CREDENTIAL_NAME);
|
||||
});
|
||||
|
||||
it('should set a default credential when adding nodes', () => {
|
||||
workflowPage.actions.visit();
|
||||
|
||||
createNotionCredential();
|
||||
|
||||
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
|
||||
workflowPage.getters
|
||||
.nodeCredentialsSelect()
|
||||
.find('input')
|
||||
.should('have.value', NEW_NOTION_ACCOUNT_NAME);
|
||||
|
||||
deleteSelectedCredential();
|
||||
});
|
||||
|
||||
it('should set a default credential when editing a node', () => {
|
||||
workflowPage.actions.visit();
|
||||
|
||||
createNotionCredential();
|
||||
|
||||
workflowPage.actions.addNodeToCanvas(HTTP_REQUEST_NODE_NAME, true, true);
|
||||
nodeDetailsView.getters.parameterInput('authentication').click();
|
||||
getVisibleSelect().find('li').contains('Predefined').click();
|
||||
|
||||
nodeDetailsView.getters.parameterInput('nodeCredentialType').click();
|
||||
getVisibleSelect().find('li').contains('Notion API').click();
|
||||
|
||||
workflowPage.getters
|
||||
.nodeCredentialsSelect()
|
||||
.find('input')
|
||||
.should('have.value', NEW_NOTION_ACCOUNT_NAME);
|
||||
|
||||
deleteSelectedCredential();
|
||||
});
|
||||
|
||||
it('should setup generic authentication for HTTP node', () => {
|
||||
workflowPage.actions.visit();
|
||||
workflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
|
@ -278,4 +328,25 @@ describe('Credentials', () => {
|
|||
credentialsModal.getters.credentialAuthTypeRadioButtons().first().click();
|
||||
nodeDetailsView.getters.copyInput().should('not.exist');
|
||||
});
|
||||
|
||||
it('ADO-2583 should show notifications above credential modal overlay', () => {
|
||||
// check error notifications because they are sticky
|
||||
cy.intercept('POST', '/rest/credentials', { forceNetworkError: true });
|
||||
credentialsPage.getters.createCredentialButton().click();
|
||||
|
||||
credentialsModal.getters.newCredentialModal().should('be.visible');
|
||||
credentialsModal.getters.newCredentialTypeSelect().should('be.visible');
|
||||
credentialsModal.getters.newCredentialTypeOption('Notion API').click();
|
||||
|
||||
credentialsModal.getters.newCredentialTypeButton().click();
|
||||
credentialsModal.getters.connectionParameter('Internal Integration Secret').type('1234567890');
|
||||
|
||||
credentialsModal.actions.setName('My awesome Notion account');
|
||||
credentialsModal.getters.saveButton().click({ force: true });
|
||||
errorToast().should('have.length', 1);
|
||||
errorToast().should('be.visible');
|
||||
|
||||
errorToast().should('have.css', 'z-index', '2100');
|
||||
cy.get('.el-overlay').should('have.css', 'z-index', '2001');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import { WEBHOOK_NODE_NAME } from '../constants';
|
||||
import { NDV, WorkflowPage } from '../pages';
|
||||
|
||||
const workflowPage = new WorkflowPage();
|
||||
const ndv = new NDV();
|
||||
|
||||
describe('ADO-2270 Save button resets on webhook node open', () => {
|
||||
it('should not reset the save button if webhook node is opened and closed', () => {
|
||||
workflowPage.actions.visit();
|
||||
workflowPage.actions.addInitialNodeToCanvas(WEBHOOK_NODE_NAME);
|
||||
workflowPage.getters.saveButton().click();
|
||||
workflowPage.actions.openNode(WEBHOOK_NODE_NAME);
|
||||
|
||||
ndv.actions.close();
|
||||
|
||||
cy.ifCanvasVersion(
|
||||
() => cy.getByTestId('workflow-save-button').should('not.contain', 'Saved'),
|
||||
() => cy.getByTestId('workflow-save-button').should('contain', 'Saved'),
|
||||
);
|
||||
});
|
||||
});
|
|
@ -90,6 +90,14 @@ function createRunDataWithError(inputMessage: string) {
|
|||
routine: 'InitPostgres',
|
||||
} as unknown as Error,
|
||||
} as ExecutionError,
|
||||
metadata: {
|
||||
subRun: [
|
||||
{
|
||||
node: 'Postgres Chat Memory',
|
||||
runIndex: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
createMockNodeExecutionData(AGENT_NODE_NAME, {
|
||||
executionStatus: 'error',
|
||||
|
@ -124,14 +132,6 @@ function createRunDataWithError(inputMessage: string) {
|
|||
description: 'Internal error',
|
||||
message: 'Internal error',
|
||||
} as unknown as ExecutionError,
|
||||
metadata: {
|
||||
subRun: [
|
||||
{
|
||||
node: 'Postgres Chat Memory',
|
||||
runIndex: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
|
86
cypress/e2e/2372-ado-prevent-clipping-params.cy.ts
Normal file
86
cypress/e2e/2372-ado-prevent-clipping-params.cy.ts
Normal file
|
@ -0,0 +1,86 @@
|
|||
import { NDV, WorkflowPage } from '../pages';
|
||||
|
||||
const workflowPage = new WorkflowPage();
|
||||
const ndv = new NDV();
|
||||
|
||||
describe('ADO-2362 ADO-2350 NDV Prevent clipping long parameters and scrolling to expression', () => {
|
||||
it('should show last parameters and open at scroll top of parameters', () => {
|
||||
workflowPage.actions.visit();
|
||||
cy.createFixtureWorkflow('Test-workflow-with-long-parameters.json');
|
||||
workflowPage.actions.openNode('Schedule Trigger');
|
||||
|
||||
ndv.getters.inlineExpressionEditorInput().should('be.visible');
|
||||
|
||||
ndv.actions.close();
|
||||
|
||||
workflowPage.actions.openNode('Edit Fields1');
|
||||
|
||||
// first parameter should be visible
|
||||
ndv.getters.inputLabel().eq(0).should('include.text', 'Mode');
|
||||
ndv.getters.inputLabel().eq(0).should('be.visible');
|
||||
|
||||
ndv.getters.inlineExpressionEditorInput().should('have.length', 2);
|
||||
|
||||
// last parameter in view should be visible
|
||||
ndv.getters.inlineExpressionEditorInput().eq(0).should('have.text', 'should be visible!');
|
||||
ndv.getters.inlineExpressionEditorInput().eq(0).should('be.visible');
|
||||
|
||||
// next parameter in view should not be visible
|
||||
ndv.getters.inlineExpressionEditorInput().eq(1).should('have.text', 'not visible');
|
||||
ndv.getters.inlineExpressionEditorInput().eq(1).should('not.be.visible');
|
||||
|
||||
ndv.actions.close();
|
||||
workflowPage.actions.openNode('Schedule Trigger');
|
||||
|
||||
// first parameter (notice) should be visible
|
||||
ndv.getters.nthParam(0).should('include.text', 'This workflow will run on the schedule ');
|
||||
ndv.getters.inputLabel().eq(0).should('be.visible');
|
||||
|
||||
ndv.getters.inlineExpressionEditorInput().should('have.length', 2);
|
||||
|
||||
// last parameter in view should be visible
|
||||
ndv.getters.inlineExpressionEditorInput().eq(0).should('have.text', 'should be visible');
|
||||
ndv.getters.inlineExpressionEditorInput().eq(0).should('be.visible');
|
||||
|
||||
// next parameter in view should not be visible
|
||||
ndv.getters.inlineExpressionEditorInput().eq(1).should('have.text', 'not visible');
|
||||
ndv.getters.inlineExpressionEditorInput().eq(1).should('not.be.visible');
|
||||
|
||||
ndv.actions.close();
|
||||
workflowPage.actions.openNode('Slack');
|
||||
|
||||
// first field (credentials) should be visible
|
||||
ndv.getters.nodeCredentialsLabel().should('be.visible');
|
||||
|
||||
// last parameter in view should be visible
|
||||
ndv.getters.inlineExpressionEditorInput().eq(0).should('have.text', 'should be visible');
|
||||
ndv.getters.inlineExpressionEditorInput().eq(0).should('be.visible');
|
||||
|
||||
// next parameter in view should not be visible
|
||||
ndv.getters.inlineExpressionEditorInput().eq(1).should('have.text', 'not visible');
|
||||
ndv.getters.inlineExpressionEditorInput().eq(1).should('not.be.visible');
|
||||
});
|
||||
|
||||
it('NODE-1272 ensure expressions scrolled to top, not middle', () => {
|
||||
workflowPage.actions.visit();
|
||||
cy.createFixtureWorkflow('Test-workflow-with-long-parameters.json');
|
||||
workflowPage.actions.openNode('With long expression');
|
||||
|
||||
ndv.getters.inlineExpressionEditorInput().eq(0).should('be.visible');
|
||||
// should be scrolled at top
|
||||
ndv.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.eq(0)
|
||||
.find('.cm-line')
|
||||
.eq(0)
|
||||
.should('have.text', '1 visible!');
|
||||
ndv.getters.inlineExpressionEditorInput().eq(0).find('.cm-line').eq(0).should('be.visible');
|
||||
ndv.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.eq(0)
|
||||
.find('.cm-line')
|
||||
.eq(6)
|
||||
.should('have.text', '7 not visible!');
|
||||
ndv.getters.inlineExpressionEditorInput().eq(0).find('.cm-line').eq(6).should('not.be.visible');
|
||||
});
|
||||
});
|
|
@ -117,7 +117,8 @@ describe('Debug', () => {
|
|||
workflowPage.getters.canvasNodes().last().find('.node-info-icon').should('be.empty');
|
||||
|
||||
workflowPage.getters.canvasNodes().first().dblclick();
|
||||
ndv.getters.pinDataButton().click();
|
||||
ndv.actions.unPinData();
|
||||
|
||||
ndv.actions.close();
|
||||
|
||||
workflowPage.actions.saveWorkflowUsingKeyboardShortcut();
|
||||
|
|
|
@ -44,6 +44,7 @@ import {
|
|||
openNode,
|
||||
getConnectionBySourceAndTarget,
|
||||
} from '../composables/workflow';
|
||||
import { NDV, WorkflowPage } from '../pages';
|
||||
import { createMockNodeExecutionData, runMockWorkflowExecution } from '../utils';
|
||||
|
||||
describe('Langchain Integration', () => {
|
||||
|
@ -232,95 +233,96 @@ describe('Langchain Integration', () => {
|
|||
|
||||
const inputMessage = 'Hello!';
|
||||
const outputMessage = 'Hi there! How can I assist you today?';
|
||||
const runData = [
|
||||
createMockNodeExecutionData(MANUAL_CHAT_TRIGGER_NODE_NAME, {
|
||||
jsonData: {
|
||||
main: { input: inputMessage },
|
||||
},
|
||||
}),
|
||||
createMockNodeExecutionData(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, {
|
||||
jsonData: {
|
||||
ai_languageModel: {
|
||||
response: {
|
||||
generations: [
|
||||
{
|
||||
text: `{
|
||||
"action": "Final Answer",
|
||||
"action_input": "${outputMessage}"
|
||||
}`,
|
||||
message: {
|
||||
lc: 1,
|
||||
type: 'constructor',
|
||||
id: ['langchain', 'schema', 'AIMessage'],
|
||||
kwargs: {
|
||||
content: `{
|
||||
"action": "Final Answer",
|
||||
"action_input": "${outputMessage}"
|
||||
}`,
|
||||
additional_kwargs: {},
|
||||
},
|
||||
},
|
||||
generationInfo: { finish_reason: 'stop' },
|
||||
},
|
||||
],
|
||||
llmOutput: {
|
||||
tokenUsage: {
|
||||
completionTokens: 26,
|
||||
promptTokens: 519,
|
||||
totalTokens: 545,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
subRun: [{ node: AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, runIndex: 0 }],
|
||||
},
|
||||
inputOverride: {
|
||||
ai_languageModel: [
|
||||
[
|
||||
{
|
||||
json: {
|
||||
messages: [
|
||||
{
|
||||
lc: 1,
|
||||
type: 'constructor',
|
||||
id: ['langchain', 'schema', 'SystemMessage'],
|
||||
kwargs: {
|
||||
content:
|
||||
'Assistant is a large language model trained by OpenAI.\n\nAssistant is designed to be able to assist with a wide range of tasks, from answering simple questions to providing in-depth explanations and discussions on a wide range of topics. As a language model, Assistant is able to generate human-like text based on the input it receives, allowing it to engage in natural-sounding conversations and provide responses that are coherent and relevant to the topic at hand.\n\nAssistant is constantly learning and improving, and its capabilities are constantly evolving. It is able to process and understand large amounts of text, and can use this knowledge to provide accurate and informative responses to a wide range of questions. Additionally, Assistant is able to generate its own text based on the input it receives, allowing it to engage in discussions and provide explanations and descriptions on a wide range of topics.\n\nOverall, Assistant is a powerful system that can help with a wide range of tasks and provide valuable insights and information on a wide range of topics. Whether you need help with a specific question or just want to have a conversation about a particular topic, Assistant is here to assist. However, above all else, all responses must adhere to the format of RESPONSE FORMAT INSTRUCTIONS.',
|
||||
additional_kwargs: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
lc: 1,
|
||||
type: 'constructor',
|
||||
id: ['langchain', 'schema', 'HumanMessage'],
|
||||
kwargs: {
|
||||
content:
|
||||
'TOOLS\n------\nAssistant can ask the user to use tools to look up information that may be helpful in answering the users original question. The tools the human can use are:\n\n\n\nRESPONSE FORMAT INSTRUCTIONS\n----------------------------\n\nOutput a JSON markdown code snippet containing a valid JSON object in one of two formats:\n\n**Option 1:**\nUse this if you want the human to use a tool.\nMarkdown code snippet formatted in the following schema:\n\n```json\n{\n "action": string, // The action to take. Must be one of []\n "action_input": string // The input to the action. May be a stringified object.\n}\n```\n\n**Option #2:**\nUse this if you want to respond directly and conversationally to the human. Markdown code snippet formatted in the following schema:\n\n```json\n{\n "action": "Final Answer",\n "action_input": string // You should put what you want to return to use here and make sure to use valid json newline characters.\n}\n```\n\nFor both options, remember to always include the surrounding markdown code snippet delimiters (begin with "```json" and end with "```")!\n\n\nUSER\'S INPUT\n--------------------\nHere is the user\'s input (remember to respond with a markdown code snippet of a json blob with a single action, and NOTHING else):\n\nHello!',
|
||||
additional_kwargs: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
options: { stop: ['Observation:'], promptIndex: 0 },
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
}),
|
||||
createMockNodeExecutionData(AGENT_NODE_NAME, {
|
||||
jsonData: {
|
||||
main: { output: 'Hi there! How can I assist you today?' },
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
runMockWorkflowExecution({
|
||||
trigger: () => {
|
||||
sendManualChatMessage(inputMessage);
|
||||
},
|
||||
runData: [
|
||||
createMockNodeExecutionData(MANUAL_CHAT_TRIGGER_NODE_NAME, {
|
||||
jsonData: {
|
||||
main: { input: inputMessage },
|
||||
},
|
||||
}),
|
||||
createMockNodeExecutionData(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, {
|
||||
jsonData: {
|
||||
ai_languageModel: {
|
||||
response: {
|
||||
generations: [
|
||||
{
|
||||
text: `{
|
||||
"action": "Final Answer",
|
||||
"action_input": "${outputMessage}"
|
||||
}`,
|
||||
message: {
|
||||
lc: 1,
|
||||
type: 'constructor',
|
||||
id: ['langchain', 'schema', 'AIMessage'],
|
||||
kwargs: {
|
||||
content: `{
|
||||
"action": "Final Answer",
|
||||
"action_input": "${outputMessage}"
|
||||
}`,
|
||||
additional_kwargs: {},
|
||||
},
|
||||
},
|
||||
generationInfo: { finish_reason: 'stop' },
|
||||
},
|
||||
],
|
||||
llmOutput: {
|
||||
tokenUsage: {
|
||||
completionTokens: 26,
|
||||
promptTokens: 519,
|
||||
totalTokens: 545,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
inputOverride: {
|
||||
ai_languageModel: [
|
||||
[
|
||||
{
|
||||
json: {
|
||||
messages: [
|
||||
{
|
||||
lc: 1,
|
||||
type: 'constructor',
|
||||
id: ['langchain', 'schema', 'SystemMessage'],
|
||||
kwargs: {
|
||||
content:
|
||||
'Assistant is a large language model trained by OpenAI.\n\nAssistant is designed to be able to assist with a wide range of tasks, from answering simple questions to providing in-depth explanations and discussions on a wide range of topics. As a language model, Assistant is able to generate human-like text based on the input it receives, allowing it to engage in natural-sounding conversations and provide responses that are coherent and relevant to the topic at hand.\n\nAssistant is constantly learning and improving, and its capabilities are constantly evolving. It is able to process and understand large amounts of text, and can use this knowledge to provide accurate and informative responses to a wide range of questions. Additionally, Assistant is able to generate its own text based on the input it receives, allowing it to engage in discussions and provide explanations and descriptions on a wide range of topics.\n\nOverall, Assistant is a powerful system that can help with a wide range of tasks and provide valuable insights and information on a wide range of topics. Whether you need help with a specific question or just want to have a conversation about a particular topic, Assistant is here to assist. However, above all else, all responses must adhere to the format of RESPONSE FORMAT INSTRUCTIONS.',
|
||||
additional_kwargs: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
lc: 1,
|
||||
type: 'constructor',
|
||||
id: ['langchain', 'schema', 'HumanMessage'],
|
||||
kwargs: {
|
||||
content:
|
||||
'TOOLS\n------\nAssistant can ask the user to use tools to look up information that may be helpful in answering the users original question. The tools the human can use are:\n\n\n\nRESPONSE FORMAT INSTRUCTIONS\n----------------------------\n\nOutput a JSON markdown code snippet containing a valid JSON object in one of two formats:\n\n**Option 1:**\nUse this if you want the human to use a tool.\nMarkdown code snippet formatted in the following schema:\n\n```json\n{\n "action": string, // The action to take. Must be one of []\n "action_input": string // The input to the action. May be a stringified object.\n}\n```\n\n**Option #2:**\nUse this if you want to respond directly and conversationally to the human. Markdown code snippet formatted in the following schema:\n\n```json\n{\n "action": "Final Answer",\n "action_input": string // You should put what you want to return to use here and make sure to use valid json newline characters.\n}\n```\n\nFor both options, remember to always include the surrounding markdown code snippet delimiters (begin with "```json" and end with "```")!\n\n\nUSER\'S INPUT\n--------------------\nHere is the user\'s input (remember to respond with a markdown code snippet of a json blob with a single action, and NOTHING else):\n\nHello!',
|
||||
additional_kwargs: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
options: { stop: ['Observation:'], promptIndex: 0 },
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
}),
|
||||
createMockNodeExecutionData(AGENT_NODE_NAME, {
|
||||
jsonData: {
|
||||
main: { output: 'Hi there! How can I assist you today?' },
|
||||
},
|
||||
metadata: {
|
||||
subRun: [{ node: AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, runIndex: 0 }],
|
||||
},
|
||||
}),
|
||||
],
|
||||
runData,
|
||||
lastNodeExecuted: AGENT_NODE_NAME,
|
||||
});
|
||||
|
||||
|
@ -357,4 +359,56 @@ describe('Langchain Integration', () => {
|
|||
getConnectionBySourceAndTarget(CHAT_TRIGGER_NODE_DISPLAY_NAME, AGENT_NODE_NAME).should('exist');
|
||||
getNodes().should('have.length', 3);
|
||||
});
|
||||
it('should render runItems for sub-nodes and allow switching between them', () => {
|
||||
const workflowPage = new WorkflowPage();
|
||||
const ndv = new NDV();
|
||||
|
||||
cy.visit(workflowPage.url);
|
||||
cy.createFixtureWorkflow('In_memory_vector_store_fake_embeddings.json');
|
||||
workflowPage.actions.zoomToFit();
|
||||
|
||||
workflowPage.actions.executeNode('Populate VS');
|
||||
cy.get('[data-label="25 items"]').should('exist');
|
||||
|
||||
const assertInputOutputText = (text: string, assertion: 'exist' | 'not.exist') => {
|
||||
ndv.getters.outputPanel().contains(text).should(assertion);
|
||||
ndv.getters.inputPanel().contains(text).should(assertion);
|
||||
};
|
||||
|
||||
workflowPage.actions.openNode('Character Text Splitter');
|
||||
ndv.getters.outputRunSelector().should('exist');
|
||||
ndv.getters.inputRunSelector().should('exist');
|
||||
ndv.getters.inputRunSelector().find('input').should('include.value', '3 of 3');
|
||||
ndv.getters.outputRunSelector().find('input').should('include.value', '3 of 3');
|
||||
assertInputOutputText('Kyiv', 'exist');
|
||||
assertInputOutputText('Berlin', 'not.exist');
|
||||
assertInputOutputText('Prague', 'not.exist');
|
||||
|
||||
ndv.actions.changeOutputRunSelector('2 of 3');
|
||||
assertInputOutputText('Berlin', 'exist');
|
||||
assertInputOutputText('Kyiv', 'not.exist');
|
||||
assertInputOutputText('Prague', 'not.exist');
|
||||
|
||||
ndv.actions.changeOutputRunSelector('1 of 3');
|
||||
assertInputOutputText('Prague', 'exist');
|
||||
assertInputOutputText('Berlin', 'not.exist');
|
||||
assertInputOutputText('Kyiv', 'not.exist');
|
||||
|
||||
ndv.actions.toggleInputRunLinking();
|
||||
ndv.actions.changeOutputRunSelector('2 of 3');
|
||||
ndv.getters.inputRunSelector().find('input').should('include.value', '1 of 3');
|
||||
ndv.getters.outputRunSelector().find('input').should('include.value', '2 of 3');
|
||||
ndv.getters.inputPanel().contains('Prague').should('exist');
|
||||
ndv.getters.inputPanel().contains('Berlin').should('not.exist');
|
||||
|
||||
ndv.getters.outputPanel().contains('Berlin').should('exist');
|
||||
ndv.getters.outputPanel().contains('Prague').should('not.exist');
|
||||
|
||||
ndv.actions.toggleInputRunLinking();
|
||||
ndv.getters.inputRunSelector().find('input').should('include.value', '1 of 3');
|
||||
ndv.getters.outputRunSelector().find('input').should('include.value', '1 of 3');
|
||||
assertInputOutputText('Prague', 'exist');
|
||||
assertInputOutputText('Berlin', 'not.exist');
|
||||
assertInputOutputText('Kyiv', 'not.exist');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,21 +1,29 @@
|
|||
import workflow from '../fixtures/Manual_wait_set.json';
|
||||
import { getOutputTableRow } from '../composables/ndv';
|
||||
import { getCanvasNodes, openNode } from '../composables/workflow';
|
||||
import SIMPLE_WORKFLOW from '../fixtures/Manual_wait_set.json';
|
||||
import WORKFLOW_WITH_PINNED from '../fixtures/Webhook_set_pinned.json';
|
||||
import { importWorkflow, visitDemoPage } from '../pages/demo';
|
||||
import { errorToast } from '../pages/notifications';
|
||||
import { WorkflowPage } from '../pages/workflow';
|
||||
|
||||
const workflowPage = new WorkflowPage();
|
||||
|
||||
describe('Demo', () => {
|
||||
beforeEach(() => {
|
||||
cy.overrideSettings({ previewMode: true });
|
||||
cy.signout();
|
||||
});
|
||||
|
||||
it('can import template', () => {
|
||||
visitDemoPage();
|
||||
errorToast().should('not.exist');
|
||||
importWorkflow(workflow);
|
||||
workflowPage.getters.canvasNodes().should('have.length', 3);
|
||||
importWorkflow(SIMPLE_WORKFLOW);
|
||||
getCanvasNodes().should('have.length', 3);
|
||||
});
|
||||
|
||||
it('can import workflow with pin data', () => {
|
||||
visitDemoPage();
|
||||
importWorkflow(WORKFLOW_WITH_PINNED);
|
||||
getCanvasNodes().should('have.length', 2);
|
||||
openNode('Webhook');
|
||||
getOutputTableRow(0).should('include.text', 'headers');
|
||||
getOutputTableRow(1).should('include.text', 'dragons');
|
||||
});
|
||||
|
||||
it('can override theme to dark', () => {
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
NDV,
|
||||
MainSidebar,
|
||||
} from '../pages';
|
||||
import { clearNotifications } from '../pages/notifications';
|
||||
import { getVisibleDropdown, getVisibleModalOverlay, getVisibleSelect } from '../utils';
|
||||
|
||||
const workflowsPage = new WorkflowsPage();
|
||||
|
@ -440,7 +441,9 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
.should('contain.text', 'Notion account personal project');
|
||||
});
|
||||
|
||||
it('should move resources between projects', () => {
|
||||
// Skip flaky test
|
||||
// eslint-disable-next-line n8n-local-rules/no-skipped-tests
|
||||
it.skip('should move resources between projects', () => {
|
||||
cy.signinAsOwner();
|
||||
cy.visit(workflowsPage.url);
|
||||
|
||||
|
@ -448,38 +451,48 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
workflowsPage.getters.workflowCards().should('not.have.length');
|
||||
workflowsPage.getters.newWorkflowButtonCard().click();
|
||||
projects.createWorkflow('Test_workflow_1.json', 'Workflow in Home project');
|
||||
clearNotifications();
|
||||
|
||||
projects.getHomeButton().click();
|
||||
projects.getProjectTabCredentials().should('be.visible').click();
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
projects.createCredential('Credential in Home project');
|
||||
|
||||
clearNotifications();
|
||||
|
||||
// Create a project and add a credential and a workflow to it
|
||||
projects.createProject('Project 1');
|
||||
clearNotifications();
|
||||
projects.getProjectTabCredentials().click();
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
projects.createCredential('Credential in Project 1');
|
||||
clearNotifications();
|
||||
|
||||
projects.getProjectTabWorkflows().click();
|
||||
workflowsPage.getters.newWorkflowButtonCard().click();
|
||||
projects.createWorkflow('Test_workflow_1.json', 'Workflow in Project 1');
|
||||
|
||||
clearNotifications();
|
||||
|
||||
// Create another project and add a credential and a workflow to it
|
||||
projects.createProject('Project 2');
|
||||
clearNotifications();
|
||||
projects.getProjectTabCredentials().click();
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
projects.createCredential('Credential in Project 2');
|
||||
clearNotifications();
|
||||
|
||||
projects.getProjectTabWorkflows().click();
|
||||
workflowsPage.getters.newWorkflowButtonCard().click();
|
||||
projects.createWorkflow('Test_workflow_1.json', 'Workflow in Project 2');
|
||||
clearNotifications();
|
||||
|
||||
// Move the workflow owned by me from Home to Project 1
|
||||
// Move the workflow Personal from Home to Project 1
|
||||
projects.getHomeButton().click();
|
||||
workflowsPage.getters
|
||||
.workflowCards()
|
||||
.should('have.length', 3)
|
||||
.filter(':contains("Owned by me")')
|
||||
.filter(':contains("Personal")')
|
||||
.should('exist');
|
||||
workflowsPage.getters.workflowCardActions('Workflow in Home project').click();
|
||||
workflowsPage.getters.workflowMoveButton().click();
|
||||
|
@ -496,11 +509,12 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
.filter(':contains("Project 1")')
|
||||
.click();
|
||||
projects.getResourceMoveModal().find('button:contains("Move workflow")').click();
|
||||
clearNotifications();
|
||||
|
||||
workflowsPage.getters
|
||||
.workflowCards()
|
||||
.should('have.length', 3)
|
||||
.filter(':contains("Owned by me")')
|
||||
.filter(':contains("Personal")')
|
||||
.should('not.exist');
|
||||
|
||||
// Move the workflow from Project 1 to Project 2
|
||||
|
@ -527,6 +541,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
workflowsPage.getters.workflowCards().should('have.length', 2);
|
||||
workflowsPage.getters.workflowCardActions('Workflow in Home project').click();
|
||||
workflowsPage.getters.workflowMoveButton().click();
|
||||
clearNotifications();
|
||||
|
||||
projects
|
||||
.getResourceMoveModal()
|
||||
|
@ -566,10 +581,11 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
.click();
|
||||
|
||||
projects.getResourceMoveModal().find('button:contains("Move workflow")').click();
|
||||
clearNotifications();
|
||||
workflowsPage.getters
|
||||
.workflowCards()
|
||||
.should('have.length', 3)
|
||||
.filter(':contains("Owned by me")')
|
||||
.filter(':contains("Personal")')
|
||||
.should('have.length', 1);
|
||||
|
||||
// Move the credential from Project 1 to Project 2
|
||||
|
@ -591,7 +607,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
.filter(':contains("Project 2")')
|
||||
.click();
|
||||
projects.getResourceMoveModal().find('button:contains("Move credential")').click();
|
||||
|
||||
clearNotifications();
|
||||
credentialsPage.getters.credentialCards().should('not.have.length');
|
||||
|
||||
// Move the credential from Project 2 to admin user
|
||||
|
@ -637,10 +653,12 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
.click();
|
||||
projects.getResourceMoveModal().find('button:contains("Move credential")').click();
|
||||
|
||||
clearNotifications();
|
||||
|
||||
credentialsPage.getters
|
||||
.credentialCards()
|
||||
.should('have.length', 3)
|
||||
.filter(':contains("Owned by me")')
|
||||
.filter(':contains("Personal")')
|
||||
.should('have.length', 2);
|
||||
|
||||
// Move the credential from admin user back to its original project (Project 1)
|
||||
|
@ -668,7 +686,9 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
.should('have.length', 1);
|
||||
});
|
||||
|
||||
it('should allow to change inaccessible credential when the workflow was moved to a team project', () => {
|
||||
// Skip flaky test
|
||||
// eslint-disable-next-line n8n-local-rules/no-skipped-tests
|
||||
it.skip('should allow to change inaccessible credential when the workflow was moved to a team project', () => {
|
||||
cy.signinAsOwner();
|
||||
cy.visit(workflowsPage.url);
|
||||
|
||||
|
@ -699,7 +719,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
workflowsPage.getters
|
||||
.workflowCards()
|
||||
.should('have.length', 1)
|
||||
.filter(':contains("Owned by me")')
|
||||
.filter(':contains("Personal")')
|
||||
.should('exist');
|
||||
workflowsPage.getters.workflowCardActions('My workflow').click();
|
||||
workflowsPage.getters.workflowMoveButton().click();
|
||||
|
@ -720,7 +740,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
workflowsPage.getters
|
||||
.workflowCards()
|
||||
.should('have.length', 1)
|
||||
.filter(':contains("Owned by me")')
|
||||
.filter(':contains("Personal")')
|
||||
.should('not.exist');
|
||||
|
||||
//Log out with instance owner and log in with the member user
|
||||
|
|
|
@ -23,6 +23,7 @@ describe('Manual partial execution', () => {
|
|||
canvas.actions.openNode('Webhook1');
|
||||
|
||||
ndv.getters.nodeRunSuccessIndicator().should('exist');
|
||||
ndv.getters.nodeRunTooltipIndicator().should('exist');
|
||||
ndv.getters.outputRunSelector().should('not.exist'); // single run
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,6 +4,7 @@ import { clickCreateNewCredential, openCredentialSelect } from '../composables/n
|
|||
import { GMAIL_NODE_NAME, SCHEDULE_TRIGGER_NODE_NAME } from '../constants';
|
||||
import { CredentialsModal, CredentialsPage, NDV, WorkflowPage } from '../pages';
|
||||
import { AIAssistant } from '../pages/features/ai-assistant';
|
||||
import { NodeCreator } from '../pages/features/node-creator';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
|
||||
const wf = new WorkflowPage();
|
||||
|
@ -11,6 +12,7 @@ const ndv = new NDV();
|
|||
const aiAssistant = new AIAssistant();
|
||||
const credentialsPage = new CredentialsPage();
|
||||
const credentialsModal = new CredentialsModal();
|
||||
const nodeCreatorFeature = new NodeCreator();
|
||||
|
||||
describe('AI Assistant::disabled', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -145,42 +147,6 @@ describe('AI Assistant::enabled', () => {
|
|||
aiAssistant.getters.chatMessagesUser().eq(0).should('contain.text', "Sure, let's do it");
|
||||
});
|
||||
|
||||
it('should show quick replies when node is executed after new suggestion', () => {
|
||||
cy.intercept('POST', '/rest/ai/chat', (req) => {
|
||||
req.reply((res) => {
|
||||
if (['init-error-helper', 'message'].includes(req.body.payload.type)) {
|
||||
res.send({
|
||||
statusCode: 200,
|
||||
fixture: 'aiAssistant/responses/simple_message_response.json',
|
||||
});
|
||||
} else if (req.body.payload.type === 'event') {
|
||||
res.send({
|
||||
statusCode: 200,
|
||||
fixture: 'aiAssistant/responses/node_execution_error_response.json',
|
||||
});
|
||||
} else {
|
||||
res.send({ statusCode: 500 });
|
||||
}
|
||||
});
|
||||
}).as('chatRequest');
|
||||
cy.createFixtureWorkflow('aiAssistant/workflows/test_workflow.json');
|
||||
wf.actions.openNode('Edit Fields');
|
||||
ndv.getters.nodeExecuteButton().click();
|
||||
aiAssistant.getters.nodeErrorViewAssistantButton().click();
|
||||
cy.wait('@chatRequest');
|
||||
aiAssistant.getters.chatMessagesAssistant().should('have.length', 1);
|
||||
ndv.getters.nodeExecuteButton().click();
|
||||
cy.wait('@chatRequest');
|
||||
// Respond 'Yes' to the quick reply (request new suggestion)
|
||||
aiAssistant.getters.quickReplies().contains('Yes').click();
|
||||
cy.wait('@chatRequest');
|
||||
// No quick replies at this point
|
||||
aiAssistant.getters.quickReplies().should('not.exist');
|
||||
ndv.getters.nodeExecuteButton().click();
|
||||
// But after executing the node again, quick replies should be shown
|
||||
aiAssistant.getters.quickReplies().should('have.length', 2);
|
||||
});
|
||||
|
||||
it('should warn before starting a new session', () => {
|
||||
cy.intercept('POST', '/rest/ai/chat', {
|
||||
statusCode: 200,
|
||||
|
@ -316,6 +282,20 @@ describe('AI Assistant::enabled', () => {
|
|||
wf.getters.isWorkflowSaved();
|
||||
aiAssistant.getters.placeholderMessage().should('not.exist');
|
||||
});
|
||||
|
||||
it('should send message via enter even with global NodeCreator panel opened', () => {
|
||||
cy.intercept('POST', '/rest/ai/chat', {
|
||||
statusCode: 200,
|
||||
fixture: 'aiAssistant/responses/simple_message_response.json',
|
||||
}).as('chatRequest');
|
||||
|
||||
wf.actions.addInitialNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
aiAssistant.actions.openChat();
|
||||
nodeCreatorFeature.actions.openNodeCreator();
|
||||
aiAssistant.getters.chatInput().type('Hello{Enter}');
|
||||
|
||||
aiAssistant.getters.placeholderMessage().should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe('AI Assistant Credential Help', () => {
|
||||
|
|
|
@ -133,9 +133,10 @@ describe('NDV', () => {
|
|||
"An expression here won't work because it uses .item and n8n can't figure out the matching item.",
|
||||
);
|
||||
ndv.getters.nodeRunErrorIndicator().should('be.visible');
|
||||
ndv.getters.nodeRunTooltipIndicator().should('be.visible');
|
||||
// The error details should be hidden behind a tooltip
|
||||
ndv.getters.nodeRunErrorIndicator().should('not.contain', 'Start Time');
|
||||
ndv.getters.nodeRunErrorIndicator().should('not.contain', 'Execution Time');
|
||||
ndv.getters.nodeRunTooltipIndicator().should('not.contain', 'Start Time');
|
||||
ndv.getters.nodeRunTooltipIndicator().should('not.contain', 'Execution Time');
|
||||
});
|
||||
|
||||
it('should save workflow using keyboard shortcut from NDV', () => {
|
||||
|
@ -617,8 +618,10 @@ describe('NDV', () => {
|
|||
// Should not show run info before execution
|
||||
ndv.getters.nodeRunSuccessIndicator().should('not.exist');
|
||||
ndv.getters.nodeRunErrorIndicator().should('not.exist');
|
||||
ndv.getters.nodeRunTooltipIndicator().should('not.exist');
|
||||
ndv.getters.nodeExecuteButton().click();
|
||||
ndv.getters.nodeRunSuccessIndicator().should('exist');
|
||||
ndv.getters.nodeRunTooltipIndicator().should('exist');
|
||||
});
|
||||
|
||||
it('should properly show node execution indicator for multiple nodes', () => {
|
||||
|
@ -630,6 +633,7 @@ describe('NDV', () => {
|
|||
// Manual tigger node should show success indicator
|
||||
workflowPage.actions.openNode('When clicking ‘Test workflow’');
|
||||
ndv.getters.nodeRunSuccessIndicator().should('exist');
|
||||
ndv.getters.nodeRunTooltipIndicator().should('exist');
|
||||
// Code node should show error
|
||||
ndv.getters.backToCanvas().click();
|
||||
workflowPage.actions.openNode('Code');
|
||||
|
@ -791,4 +795,46 @@ describe('NDV', () => {
|
|||
.find('[data-test-id=run-data-schema-item]')
|
||||
.should('contain.text', 'onlyOnItem3');
|
||||
});
|
||||
|
||||
it('should keep search expanded after Test step node run', () => {
|
||||
cy.createFixtureWorkflow('Test_ndv_search.json');
|
||||
workflowPage.actions.zoomToFit();
|
||||
workflowPage.actions.executeWorkflow();
|
||||
workflowPage.actions.openNode('Edit Fields');
|
||||
ndv.getters.outputPanel().should('be.visible');
|
||||
ndv.getters.outputPanel().findChildByTestId('ndv-search').click().type('US');
|
||||
ndv.getters.outputTableRow(1).find('mark').should('have.text', 'US');
|
||||
|
||||
ndv.actions.execute();
|
||||
ndv.getters
|
||||
.outputPanel()
|
||||
.findChildByTestId('ndv-search')
|
||||
.should('be.visible')
|
||||
.should('have.value', 'US');
|
||||
});
|
||||
|
||||
it('should not show items count when seaching in schema view', () => {
|
||||
cy.createFixtureWorkflow('Test_ndv_search.json');
|
||||
workflowPage.actions.zoomToFit();
|
||||
workflowPage.actions.openNode('Edit Fields');
|
||||
ndv.getters.outputPanel().should('be.visible');
|
||||
ndv.actions.execute();
|
||||
ndv.actions.switchOutputMode('Schema');
|
||||
ndv.getters.outputPanel().find('[data-test-id=ndv-search]').click().type('US');
|
||||
ndv.getters.outputPanel().find('[data-test-id=ndv-items-count]').should('not.exist');
|
||||
});
|
||||
|
||||
it('should show additional tooltip when seaching in schema view if no matches', () => {
|
||||
cy.createFixtureWorkflow('Test_ndv_search.json');
|
||||
workflowPage.actions.zoomToFit();
|
||||
workflowPage.actions.openNode('Edit Fields');
|
||||
ndv.getters.outputPanel().should('be.visible');
|
||||
ndv.actions.execute();
|
||||
ndv.actions.switchOutputMode('Schema');
|
||||
ndv.getters.outputPanel().find('[data-test-id=ndv-search]').click().type('foo');
|
||||
ndv.getters
|
||||
.outputPanel()
|
||||
.contains('To search field contents rather than just names, use Table or JSON view')
|
||||
.should('exist');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -162,21 +162,21 @@ return []
|
|||
cy.get('#tab-code').should('have.class', 'is-active');
|
||||
});
|
||||
|
||||
it('should show error based on status code', () => {
|
||||
const prompt = nanoid(20);
|
||||
cy.get('#tab-ask-ai').click();
|
||||
ndv.actions.executePrevious();
|
||||
const handledCodes = [
|
||||
{ code: 400, message: 'Code generation failed due to an unknown reason' },
|
||||
{ code: 413, message: 'Your workflow data is too large for AI to process' },
|
||||
{ code: 429, message: "We've hit our rate limit with our AI partner" },
|
||||
{ code: 500, message: 'Code generation failed due to an unknown reason' },
|
||||
];
|
||||
|
||||
cy.getByTestId('ask-ai-prompt-input').type(prompt);
|
||||
handledCodes.forEach(({ code, message }) => {
|
||||
it(`should show error based on status code ${code}`, () => {
|
||||
const prompt = nanoid(20);
|
||||
cy.get('#tab-ask-ai').click();
|
||||
ndv.actions.executePrevious();
|
||||
|
||||
const handledCodes = [
|
||||
{ code: 400, message: 'Code generation failed due to an unknown reason' },
|
||||
{ code: 413, message: 'Your workflow data is too large for AI to process' },
|
||||
{ code: 429, message: "We've hit our rate limit with our AI partner" },
|
||||
{ code: 500, message: 'Code generation failed due to an unknown reason' },
|
||||
];
|
||||
cy.getByTestId('ask-ai-prompt-input').type(prompt);
|
||||
|
||||
handledCodes.forEach(({ code, message }) => {
|
||||
cy.intercept('POST', '/rest/ai/ask-ai', {
|
||||
statusCode: code,
|
||||
status: code,
|
||||
|
|
347
cypress/fixtures/In_memory_vector_store_fake_embeddings.json
Normal file
347
cypress/fixtures/In_memory_vector_store_fake_embeddings.json
Normal file
File diff suppressed because one or more lines are too long
39
cypress/fixtures/Pinned_webhook_node.json
Normal file
39
cypress/fixtures/Pinned_webhook_node.json
Normal file
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"path": "FwrbSiaua2Xmvn6-Z-7CQ",
|
||||
"options": {}
|
||||
},
|
||||
"id": "8fcc7e5f-2cef-4938-9564-eea504c20aa0",
|
||||
"name": "Webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
360,
|
||||
220
|
||||
],
|
||||
"webhookId": "9c778f2a-e882-46ed-a0e4-c8e2f76ccd65"
|
||||
}
|
||||
],
|
||||
"connections": {},
|
||||
"pinData": {
|
||||
"Webhook": [
|
||||
{
|
||||
"headers": {
|
||||
"connection": "keep-alive",
|
||||
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
|
||||
"accept": "*/*",
|
||||
"cookie": "n8n-auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjNiM2FhOTE5LWRhZDgtNDE5MS1hZWZiLTlhZDIwZTZkMjJjNiIsImhhc2giOiJ1ZVAxR1F3U2paIiwiaWF0IjoxNzI4OTE1NTQyLCJleHAiOjE3Mjk1MjAzNDJ9.fV02gpUnSiUoMxHwfB0npBjcjct7Mv9vGfj-jRTT3-I",
|
||||
"host": "localhost:5678",
|
||||
"accept-encoding": "gzip, deflate"
|
||||
},
|
||||
"params": {},
|
||||
"query": {},
|
||||
"body": {},
|
||||
"webhookUrl": "http://localhost:5678/webhook-test/FwrbSiaua2Xmvn6-Z-7CQ",
|
||||
"executionMode": "test"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
150
cypress/fixtures/Test-workflow-with-long-parameters.json
Normal file
150
cypress/fixtures/Test-workflow-with-long-parameters.json
Normal file
|
@ -0,0 +1,150 @@
|
|||
{
|
||||
"meta": {
|
||||
"instanceId": "777c68374367604fdf2a0bcfe9b1b574575ddea61aa8268e4bf034434bd7c894"
|
||||
},
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "0effebfc-fa8c-4d41-8a37-6d5695dfc9ee",
|
||||
"name": "test",
|
||||
"value": "test",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"id": "beb8723f-6333-4186-ab88-41d4e2338866",
|
||||
"name": "test",
|
||||
"value": "test",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"id": "85095836-4e94-442f-9270-e1a89008c129",
|
||||
"name": "test",
|
||||
"value": "test",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"id": "b6163f8a-bca6-4364-8b38-182df37c55cd",
|
||||
"name": "=should be visible!",
|
||||
"value": "=not visible",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "950fcdc1-9e92-410f-8377-d4240e9bf6ff",
|
||||
"name": "Edit Fields1",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [
|
||||
680,
|
||||
460
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"messageType": "block",
|
||||
"blocksUi": "blocks",
|
||||
"text": "=should be visible",
|
||||
"otherOptions": {
|
||||
"sendAsUser": "=not visible"
|
||||
}
|
||||
},
|
||||
"id": "dcf7410d-0f8e-4cdb-9819-ae275558bdaa",
|
||||
"name": "Slack",
|
||||
"type": "n8n-nodes-base.slack",
|
||||
"typeVersion": 2.2,
|
||||
"position": [
|
||||
900,
|
||||
460
|
||||
],
|
||||
"webhookId": "002b502e-31e5-4fdb-ac43-a56cfde8f82a"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"rule": {
|
||||
"interval": [
|
||||
{},
|
||||
{
|
||||
"field": "=should be visible"
|
||||
},
|
||||
{
|
||||
"field": "=not visible"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"id": "4c948a3f-19d4-4b08-a8be-f7d2964a21f4",
|
||||
"name": "Schedule Trigger",
|
||||
"type": "n8n-nodes-base.scheduleTrigger",
|
||||
"typeVersion": 1.2,
|
||||
"position": [
|
||||
460,
|
||||
460
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "5dcaab37-1146-49c6-97a3-3b2f73483270",
|
||||
"name": "object",
|
||||
"value": "=1 visible!\n2 {\n3 \"str\": \"two\",\n4 \"str_date\": \"{{ $now }}\",\n5 \"str_int\": \"1\",\n6 \"str_float\": \"1.234\",\n7 not visible!\n \"str_bool\": \"true\",\n \"str_email\": \"david@thedavid.com\",\n \"str_with_email\":\"My email is david@n8n.io\",\n \"str_json_single\":\"{'one':'two'}\",\n \"str_json_double\":\"{\\\"one\\\":\\\"two\\\"}\",\n \"bool\": true,\n \"list\": [1, 2, 3],\n \"decimal\": 1.234,\n \"timestamp1\": 1708695471,\n \"timestamp2\": 1708695471000,\n \"timestamp3\": 1708695471000000,\n \"num_one\": 1\n}",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"includeOtherFields": true,
|
||||
"options": {}
|
||||
},
|
||||
"id": "a41dfb0d-38aa-42d2-b3e2-1854090bd319",
|
||||
"name": "With long expression",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.3,
|
||||
"position": [
|
||||
1100,
|
||||
460
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Edit Fields1": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Slack",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Slack": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "With long expression",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Schedule Trigger": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Edit Fields1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {}
|
||||
}
|
135
cypress/fixtures/Test_ndv_search.json
Normal file
135
cypress/fixtures/Test_ndv_search.json
Normal file
|
@ -0,0 +1,135 @@
|
|||
{
|
||||
"name": "NDV search bugs (introduced by schema view?)",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "55635c7b-92ee-4d2d-a0c0-baff9ab071da",
|
||||
"name": "When clicking ‘Test workflow’",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"position": [
|
||||
800,
|
||||
380
|
||||
],
|
||||
"typeVersion": 1
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "getAllPeople"
|
||||
},
|
||||
"id": "4737af43-e49b-4c92-b76f-32605c047114",
|
||||
"name": "Customer Datastore (n8n training)",
|
||||
"type": "n8n-nodes-base.n8nTrainingCustomerDatastore",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1020,
|
||||
380
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": []
|
||||
},
|
||||
"includeOtherFields": true,
|
||||
"options": {}
|
||||
},
|
||||
"id": "8cc9b374-1856-4f3f-9315-08e6e27840d8",
|
||||
"name": "Edit Fields",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [
|
||||
1240,
|
||||
380
|
||||
]
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Customer Datastore (n8n training)": [
|
||||
{
|
||||
"json": {
|
||||
"id": "23423532",
|
||||
"name": "Jay Gatsby",
|
||||
"email": "gatsby@west-egg.com",
|
||||
"notes": "Keeps asking about a green light??",
|
||||
"country": "US",
|
||||
"created": "1925-04-10"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": "23423533",
|
||||
"name": "José Arcadio Buendía",
|
||||
"email": "jab@macondo.co",
|
||||
"notes": "Lots of people named after him. Very confusing",
|
||||
"country": "CO",
|
||||
"created": "1967-05-05"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": "23423534",
|
||||
"name": "Max Sendak",
|
||||
"email": "info@in-and-out-of-weeks.org",
|
||||
"notes": "Keeps rolling his terrible eyes",
|
||||
"country": "US",
|
||||
"created": "1963-04-09"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": "23423535",
|
||||
"name": "Zaphod Beeblebrox",
|
||||
"email": "captain@heartofgold.com",
|
||||
"notes": "Felt like I was talking to more than one person",
|
||||
"country": null,
|
||||
"created": "1979-10-12"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": "23423536",
|
||||
"name": "Edmund Pevensie",
|
||||
"email": "edmund@narnia.gov",
|
||||
"notes": "Passionate sailor",
|
||||
"country": "UK",
|
||||
"created": "1950-10-16"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking ‘Test workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Customer Datastore (n8n training)",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Customer Datastore (n8n training)": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Edit Fields",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "20178044-fb64-4443-88dd-e941517520d0",
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": true,
|
||||
"instanceId": "be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd"
|
||||
},
|
||||
"id": "aBVnTRON9Y2cSmse",
|
||||
"tags": []
|
||||
}
|
|
@ -20,7 +20,8 @@ export class NDV extends BasePage {
|
|||
outputDataContainer: () => this.getters.outputPanel().findChildByTestId('ndv-data-container'),
|
||||
outputDisplayMode: () =>
|
||||
this.getters.outputPanel().findChildByTestId('ndv-run-data-display-mode').first(),
|
||||
pinDataButton: () => cy.getByTestId('ndv-pin-data'),
|
||||
pinDataButton: () => this.getters.outputPanel().findChildByTestId('ndv-pin-data'),
|
||||
unpinDataLink: () => this.getters.outputPanel().findChildByTestId('ndv-unpin-data'),
|
||||
editPinnedDataButton: () => cy.getByTestId('ndv-edit-pinned-data'),
|
||||
pinnedDataEditor: () => this.getters.outputPanel().find('.cm-editor .cm-scroller .cm-content'),
|
||||
runDataPaneHeader: () => cy.getByTestId('run-data-pane-header'),
|
||||
|
@ -63,6 +64,7 @@ export class NDV extends BasePage {
|
|||
nodeRenameInput: () => cy.getByTestId('node-rename-input'),
|
||||
executePrevious: () => cy.getByTestId('execute-previous-node'),
|
||||
httpRequestNotice: () => cy.getByTestId('node-parameters-http-notice'),
|
||||
nodeCredentialsLabel: () => cy.getByTestId('credentials-label'),
|
||||
nthParam: (n: number) => cy.getByTestId('node-parameters').find('.parameter-item').eq(n),
|
||||
inputRunSelector: () => this.getters.inputPanel().findChildByTestId('run-selector'),
|
||||
inputLinkRun: () => this.getters.inputPanel().findChildByTestId('link-run'),
|
||||
|
@ -130,8 +132,9 @@ export class NDV extends BasePage {
|
|||
codeEditorFullscreenButton: () => cy.getByTestId('code-editor-fullscreen-button'),
|
||||
codeEditorDialog: () => cy.getByTestId('code-editor-fullscreen'),
|
||||
codeEditorFullscreen: () => this.getters.codeEditorDialog().find('.cm-content'),
|
||||
nodeRunSuccessIndicator: () => cy.getByTestId('node-run-info-success'),
|
||||
nodeRunErrorIndicator: () => cy.getByTestId('node-run-info-danger'),
|
||||
nodeRunTooltipIndicator: () => cy.getByTestId('node-run-info'),
|
||||
nodeRunSuccessIndicator: () => cy.getByTestId('node-run-status-success'),
|
||||
nodeRunErrorIndicator: () => cy.getByTestId('node-run-status-danger'),
|
||||
nodeRunErrorMessage: () => cy.getByTestId('node-error-message'),
|
||||
nodeRunErrorDescription: () => cy.getByTestId('node-error-description'),
|
||||
fixedCollectionParameter: (paramName: string) =>
|
||||
|
@ -146,6 +149,9 @@ export class NDV extends BasePage {
|
|||
pinData: () => {
|
||||
this.getters.pinDataButton().click({ force: true });
|
||||
},
|
||||
unPinData: () => {
|
||||
this.getters.unpinDataLink().click({ force: true });
|
||||
},
|
||||
editPinnedData: () => {
|
||||
this.getters.editPinnedDataButton().click();
|
||||
},
|
||||
|
|
|
@ -2,7 +2,7 @@ import { BasePage } from './base';
|
|||
import { NodeCreator } from './features/node-creator';
|
||||
import { META_KEY } from '../constants';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
import { getUniqueWorkflowName } from '../utils/workflowUtils';
|
||||
import { getUniqueWorkflowName, isCanvasV2 } from '../utils/workflowUtils';
|
||||
|
||||
const nodeCreator = new NodeCreator();
|
||||
export class WorkflowPage extends BasePage {
|
||||
|
@ -27,7 +27,11 @@ export class WorkflowPage extends BasePage {
|
|||
nodeCreatorSearchBar: () => cy.getByTestId('node-creator-search-bar'),
|
||||
nodeCreatorPlusButton: () => cy.getByTestId('node-creator-plus-button'),
|
||||
canvasPlusButton: () => cy.getByTestId('canvas-plus-button'),
|
||||
canvasNodes: () => cy.getByTestId('canvas-node'),
|
||||
canvasNodes: () =>
|
||||
cy.ifCanvasVersion(
|
||||
() => cy.getByTestId('canvas-node'),
|
||||
() => cy.getByTestId('canvas-node').not('[data-node-type="n8n-nodes-internal.addNodes"]'),
|
||||
),
|
||||
canvasNodeByName: (nodeName: string) =>
|
||||
this.getters.canvasNodes().filter(`:contains(${nodeName})`),
|
||||
nodeIssuesByName: (nodeName: string) =>
|
||||
|
@ -37,6 +41,17 @@ export class WorkflowPage extends BasePage {
|
|||
.should('have.length.greaterThan', 0)
|
||||
.findChildByTestId('node-issues'),
|
||||
getEndpointSelector: (type: 'input' | 'output' | 'plus', nodeName: string, index = 0) => {
|
||||
if (isCanvasV2()) {
|
||||
if (type === 'input') {
|
||||
return `[data-test-id="canvas-node-input-handle"][data-node-name="${nodeName}"][data-handle-index="${index}"]`;
|
||||
}
|
||||
if (type === 'output') {
|
||||
return `[data-test-id="canvas-node-output-handle"][data-node-name="${nodeName}"][data-handle-index="${index}"]`;
|
||||
}
|
||||
if (type === 'plus') {
|
||||
return `[data-test-id="canvas-node-output-handle"][data-node-name="${nodeName}"][data-handle-index="${index}"] [data-test-id="canvas-handle-plus"] .clickable`;
|
||||
}
|
||||
}
|
||||
return `[data-endpoint-name='${nodeName}'][data-endpoint-type='${type}'][data-input-index='${index}']`;
|
||||
},
|
||||
canvasNodeInputEndpointByName: (nodeName: string, index = 0) => {
|
||||
|
@ -46,7 +61,15 @@ export class WorkflowPage extends BasePage {
|
|||
return cy.get(this.getters.getEndpointSelector('output', nodeName, index));
|
||||
},
|
||||
canvasNodePlusEndpointByName: (nodeName: string, index = 0) => {
|
||||
return cy.get(this.getters.getEndpointSelector('plus', nodeName, index));
|
||||
return cy.ifCanvasVersion(
|
||||
() => cy.get(this.getters.getEndpointSelector('plus', nodeName, index)),
|
||||
() =>
|
||||
cy
|
||||
.get(
|
||||
`[data-test-id="canvas-node-output-handle"][data-node-name="${nodeName}"] [data-test-id="canvas-handle-plus"] .clickable`,
|
||||
)
|
||||
.eq(index),
|
||||
);
|
||||
},
|
||||
activatorSwitch: () => cy.getByTestId('workflow-activate-switch'),
|
||||
workflowMenu: () => cy.getByTestId('workflow-menu'),
|
||||
|
@ -56,13 +79,29 @@ export class WorkflowPage extends BasePage {
|
|||
expressionModalInput: () => cy.getByTestId('expression-modal-input').find('[role=textbox]'),
|
||||
expressionModalOutput: () => cy.getByTestId('expression-modal-output'),
|
||||
|
||||
nodeViewRoot: () => cy.getByTestId('node-view-root'),
|
||||
nodeViewRoot: () =>
|
||||
cy.ifCanvasVersion(
|
||||
() => cy.getByTestId('node-view-root'),
|
||||
() => this.getters.nodeView(),
|
||||
),
|
||||
copyPasteInput: () => cy.getByTestId('hidden-copy-paste'),
|
||||
nodeConnections: () => cy.get('.jtk-connector'),
|
||||
nodeConnections: () =>
|
||||
cy.ifCanvasVersion(
|
||||
() => cy.get('.jtk-connector'),
|
||||
() => cy.getByTestId('edge-label-wrapper'),
|
||||
),
|
||||
zoomToFitButton: () => cy.getByTestId('zoom-to-fit'),
|
||||
nodeEndpoints: () => cy.get('.jtk-endpoint-connected'),
|
||||
disabledNodes: () => cy.get('.node-box.disabled'),
|
||||
selectedNodes: () => this.getters.canvasNodes().filter('.jtk-drag-selected'),
|
||||
disabledNodes: () =>
|
||||
cy.ifCanvasVersion(
|
||||
() => cy.get('.node-box.disabled'),
|
||||
() => cy.get('[data-test-id="canvas-trigger-node"][class*="disabled"]'),
|
||||
),
|
||||
selectedNodes: () =>
|
||||
cy.ifCanvasVersion(
|
||||
() => this.getters.canvasNodes().filter('.jtk-drag-selected'),
|
||||
() => this.getters.canvasNodes().parent().filter('.selected'),
|
||||
),
|
||||
// Workflow menu items
|
||||
workflowMenuItemDuplicate: () => cy.getByTestId('workflow-menu-item-duplicate'),
|
||||
workflowMenuItemDownload: () => cy.getByTestId('workflow-menu-item-download'),
|
||||
|
@ -92,8 +131,21 @@ export class WorkflowPage extends BasePage {
|
|||
shareButton: () => cy.getByTestId('workflow-share-button'),
|
||||
|
||||
duplicateWorkflowModal: () => cy.getByTestId('duplicate-modal'),
|
||||
nodeViewBackground: () => cy.getByTestId('node-view-background'),
|
||||
nodeView: () => cy.getByTestId('node-view'),
|
||||
nodeViewBackground: () =>
|
||||
cy.ifCanvasVersion(
|
||||
() => cy.getByTestId('node-view-background'),
|
||||
() => cy.getByTestId('canvas'),
|
||||
),
|
||||
nodeView: () =>
|
||||
cy.ifCanvasVersion(
|
||||
() => cy.getByTestId('node-view'),
|
||||
() => cy.get('[data-test-id="canvas-wrapper"]'),
|
||||
),
|
||||
canvasViewport: () =>
|
||||
cy.ifCanvasVersion(
|
||||
() => cy.getByTestId('node-view'),
|
||||
() => cy.get('.vue-flow__transformationpane.vue-flow__container'),
|
||||
),
|
||||
inlineExpressionEditorInput: () =>
|
||||
cy.getByTestId('inline-expression-editor-input').find('[role=textbox]'),
|
||||
inlineExpressionEditorOutput: () => cy.getByTestId('inline-expression-editor-output'),
|
||||
|
@ -115,12 +167,26 @@ export class WorkflowPage extends BasePage {
|
|||
ndvParameters: () => cy.getByTestId('parameter-item'),
|
||||
nodeCredentialsLabel: () => cy.getByTestId('credentials-label'),
|
||||
getConnectionBetweenNodes: (sourceNodeName: string, targetNodeName: string) =>
|
||||
cy.get(
|
||||
`.jtk-connector[data-source-node="${sourceNodeName}"][data-target-node="${targetNodeName}"]`,
|
||||
cy.ifCanvasVersion(
|
||||
() =>
|
||||
cy.get(
|
||||
`.jtk-connector[data-source-node="${sourceNodeName}"][data-target-node="${targetNodeName}"]`,
|
||||
),
|
||||
() =>
|
||||
cy.get(
|
||||
`[data-test-id="edge-label-wrapper"][data-source-node-name="${sourceNodeName}"][data-target-node-name="${targetNodeName}"]`,
|
||||
),
|
||||
),
|
||||
getConnectionActionsBetweenNodes: (sourceNodeName: string, targetNodeName: string) =>
|
||||
cy.get(
|
||||
`.connection-actions[data-source-node="${sourceNodeName}"][data-target-node="${targetNodeName}"]`,
|
||||
cy.ifCanvasVersion(
|
||||
() =>
|
||||
cy.get(
|
||||
`.connection-actions[data-source-node="${sourceNodeName}"][data-target-node="${targetNodeName}"]`,
|
||||
),
|
||||
() =>
|
||||
cy.get(
|
||||
`[data-test-id="edge-label-wrapper"][data-source-node-name="${sourceNodeName}"][data-target-node-name="${targetNodeName}"] [data-test-id="canvas-edge-toolbar"]`,
|
||||
),
|
||||
),
|
||||
addStickyButton: () => cy.getByTestId('add-sticky-button'),
|
||||
stickies: () => cy.getByTestId('sticky'),
|
||||
|
@ -128,6 +194,18 @@ export class WorkflowPage extends BasePage {
|
|||
workflowHistoryButton: () => cy.getByTestId('workflow-history-button'),
|
||||
colors: () => cy.getByTestId('color'),
|
||||
contextMenuAction: (action: string) => cy.getByTestId(`context-menu-item-${action}`),
|
||||
getNodeLeftPosition: (element: JQuery<HTMLElement>) => {
|
||||
if (isCanvasV2()) {
|
||||
return parseFloat(element.parent().css('transform').split(',')[4]);
|
||||
}
|
||||
return parseFloat(element.css('left'));
|
||||
},
|
||||
getNodeTopPosition: (element: JQuery<HTMLElement>) => {
|
||||
if (isCanvasV2()) {
|
||||
return parseFloat(element.parent().css('transform').split(',')[5]);
|
||||
}
|
||||
return parseFloat(element.css('top'));
|
||||
},
|
||||
};
|
||||
|
||||
actions = {
|
||||
|
@ -332,7 +410,7 @@ export class WorkflowPage extends BasePage {
|
|||
pinchToZoom: (steps: number, mode: 'zoomIn' | 'zoomOut' = 'zoomIn') => {
|
||||
cy.window().then((win) => {
|
||||
// Pinch-to-zoom simulates a 'wheel' event with ctrlKey: true (same as zooming by scrolling)
|
||||
this.getters.nodeViewBackground().trigger('wheel', {
|
||||
this.getters.nodeView().trigger('wheel', {
|
||||
force: true,
|
||||
bubbles: true,
|
||||
ctrlKey: true,
|
||||
|
@ -391,9 +469,12 @@ export class WorkflowPage extends BasePage {
|
|||
action?: string,
|
||||
) => {
|
||||
this.getters.getConnectionBetweenNodes(sourceNodeName, targetNodeName).first().realHover();
|
||||
this.getters
|
||||
.getConnectionActionsBetweenNodes(sourceNodeName, targetNodeName)
|
||||
.find('.add')
|
||||
const connectionsBetweenNodes = () =>
|
||||
this.getters.getConnectionActionsBetweenNodes(sourceNodeName, targetNodeName);
|
||||
cy.ifCanvasVersion(
|
||||
() => connectionsBetweenNodes().find('.add'),
|
||||
() => connectionsBetweenNodes().get('[data-test-id="add-connection-button"]'),
|
||||
)
|
||||
.first()
|
||||
.click({ force: true });
|
||||
|
||||
|
@ -401,9 +482,12 @@ export class WorkflowPage extends BasePage {
|
|||
},
|
||||
deleteNodeBetweenNodes: (sourceNodeName: string, targetNodeName: string) => {
|
||||
this.getters.getConnectionBetweenNodes(sourceNodeName, targetNodeName).first().realHover();
|
||||
this.getters
|
||||
.getConnectionActionsBetweenNodes(sourceNodeName, targetNodeName)
|
||||
.find('.delete')
|
||||
const connectionsBetweenNodes = () =>
|
||||
this.getters.getConnectionActionsBetweenNodes(sourceNodeName, targetNodeName);
|
||||
cy.ifCanvasVersion(
|
||||
() => connectionsBetweenNodes().find('.delete'),
|
||||
() => connectionsBetweenNodes().get('[data-test-id="delete-connection-button"]'),
|
||||
)
|
||||
.first()
|
||||
.click({ force: true });
|
||||
},
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
N8N_AUTH_COOKIE,
|
||||
} from '../constants';
|
||||
import { WorkflowPage } from '../pages';
|
||||
import { getUniqueWorkflowName } from '../utils/workflowUtils';
|
||||
import { getUniqueWorkflowName, isCanvasV2 } from '../utils/workflowUtils';
|
||||
|
||||
Cypress.Commands.add('setAppDate', (targetDate: number | Date) => {
|
||||
cy.window().then((win) => {
|
||||
|
@ -26,6 +26,10 @@ Cypress.Commands.add('getByTestId', (selector, ...args) => {
|
|||
return cy.get(`[data-test-id="${selector}"]`, ...args);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('ifCanvasVersion', (getterV1, getterV2) => {
|
||||
return isCanvasV2() ? getterV2() : getterV1();
|
||||
});
|
||||
|
||||
Cypress.Commands.add(
|
||||
'createFixtureWorkflow',
|
||||
(fixtureKey: string, workflowName = getUniqueWorkflowName()) => {
|
||||
|
@ -70,6 +74,10 @@ Cypress.Commands.add('signin', ({ email, password }) => {
|
|||
})
|
||||
.then((response) => {
|
||||
Cypress.env('currentUserId', response.body.data.id);
|
||||
|
||||
cy.window().then((win) => {
|
||||
win.localStorage.setItem('NodeView.switcher.discovered', 'true'); // @TODO Remove this once the switcher is removed
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,6 +20,11 @@ beforeEach(() => {
|
|||
win.localStorage.setItem('N8N_THEME', 'light');
|
||||
win.localStorage.setItem('N8N_AUTOCOMPLETE_ONBOARDED', 'true');
|
||||
win.localStorage.setItem('N8N_MAPPING_ONBOARDED', 'true');
|
||||
|
||||
const nodeViewVersion = Cypress.env('NODE_VIEW_VERSION');
|
||||
if (nodeViewVersion) {
|
||||
win.localStorage.setItem('NodeView.version', nodeViewVersion);
|
||||
}
|
||||
});
|
||||
|
||||
cy.intercept('GET', '/rest/settings', (req) => {
|
||||
|
|
|
@ -28,6 +28,7 @@ declare global {
|
|||
selector: string,
|
||||
...args: Array<Partial<Loggable & Timeoutable & Withinable & Shadow> | undefined>
|
||||
): Chainable<JQuery<HTMLElement>>;
|
||||
ifCanvasVersion<T1, T2>(getterV1: () => T1, getterV2: () => T2): T1 | T2;
|
||||
findChildByTestId(childTestId: string): Chainable<JQuery<HTMLElement>>;
|
||||
/**
|
||||
* Creates a workflow from the given fixture and optionally renames it.
|
||||
|
|
|
@ -16,7 +16,7 @@ export function createMockNodeExecutionData(
|
|||
return {
|
||||
[name]: {
|
||||
startTime: new Date().getTime(),
|
||||
executionTime: 0,
|
||||
executionTime: 1,
|
||||
executionStatus,
|
||||
data: jsonData
|
||||
? Object.keys(jsonData).reduce((acc, key) => {
|
||||
|
@ -33,6 +33,7 @@ export function createMockNodeExecutionData(
|
|||
}, {} as ITaskDataConnections)
|
||||
: data,
|
||||
source: [null],
|
||||
inputOverride,
|
||||
...rest,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -3,3 +3,7 @@ import { nanoid } from 'nanoid';
|
|||
export function getUniqueWorkflowName(workflowNamePrefix?: string) {
|
||||
return workflowNamePrefix ? `${workflowNamePrefix} ${nanoid(12)}` : nanoid(12);
|
||||
}
|
||||
|
||||
export function isCanvasV2() {
|
||||
return Cypress.env('NODE_VIEW_VERSION') === 2;
|
||||
}
|
||||
|
|
|
@ -31,6 +31,30 @@ WORKDIR /home/node
|
|||
COPY --from=builder /compiled /usr/local/lib/node_modules/n8n
|
||||
COPY docker/images/n8n/docker-entrypoint.sh /
|
||||
|
||||
# Setup the Task Runner Launcher
|
||||
ARG TARGETPLATFORM
|
||||
ARG LAUNCHER_VERSION=0.1.1
|
||||
ENV N8N_RUNNERS_MODE=internal_launcher \
|
||||
N8N_RUNNERS_LAUNCHER_PATH=/usr/local/bin/task-runner-launcher
|
||||
COPY docker/images/n8n/n8n-task-runners.json /etc/n8n-task-runners.json
|
||||
# First, download, verify, then extract the launcher binary
|
||||
# Second, chmod with 4555 to allow the use of setuid
|
||||
# Third, create a new user and group to execute the Task Runners under
|
||||
RUN \
|
||||
if [[ "$TARGETPLATFORM" = "linux/amd64" ]]; then export ARCH_NAME="x86_64"; \
|
||||
elif [[ "$TARGETPLATFORM" = "linux/arm64" ]]; then export ARCH_NAME="aarch64"; fi; \
|
||||
mkdir /launcher-temp && \
|
||||
cd /launcher-temp && \
|
||||
wget https://github.com/n8n-io/task-runner-launcher/releases/download/${LAUNCHER_VERSION}/task-runner-launcher-$ARCH_NAME-unknown-linux-musl.zip && \
|
||||
wget https://github.com/n8n-io/task-runner-launcher/releases/download/${LAUNCHER_VERSION}/task-runner-launcher-$ARCH_NAME-unknown-linux-musl.sha256 && \
|
||||
sha256sum -c task-runner-launcher-$ARCH_NAME-unknown-linux-musl.sha256 && \
|
||||
unzip -d $(dirname ${N8N_RUNNERS_LAUNCHER_PATH}) task-runner-launcher-$ARCH_NAME-unknown-linux-musl.zip task-runner-launcher && \
|
||||
cd - && \
|
||||
rm -r /launcher-temp && \
|
||||
chmod 4555 ${N8N_RUNNERS_LAUNCHER_PATH} && \
|
||||
addgroup -g 2000 task-runner && \
|
||||
adduser -D -u 2000 -g "Task Runner User" -G task-runner task-runner
|
||||
|
||||
RUN \
|
||||
cd /usr/local/lib/node_modules/n8n && \
|
||||
npm rebuild sqlite3 && \
|
||||
|
|
|
@ -22,6 +22,30 @@ RUN set -eux; \
|
|||
find /usr/local/lib/node_modules/n8n -type f -name "*.ts" -o -name "*.js.map" -o -name "*.vue" | xargs rm -f && \
|
||||
rm -rf /root/.npm
|
||||
|
||||
# Setup the Task Runner Launcher
|
||||
ARG TARGETPLATFORM
|
||||
ARG LAUNCHER_VERSION=0.1.1
|
||||
ENV N8N_RUNNERS_MODE=internal_launcher \
|
||||
N8N_RUNNERS_LAUNCHER_PATH=/usr/local/bin/task-runner-launcher
|
||||
COPY n8n-task-runners.json /etc/n8n-task-runners.json
|
||||
# First, download, verify, then extract the launcher binary
|
||||
# Second, chmod with 4555 to allow the use of setuid
|
||||
# Third, create a new user and group to execute the Task Runners under
|
||||
RUN \
|
||||
if [[ "$TARGETPLATFORM" = "linux/amd64" ]]; then export ARCH_NAME="x86_64"; \
|
||||
elif [[ "$TARGETPLATFORM" = "linux/arm64" ]]; then export ARCH_NAME="aarch64"; fi; \
|
||||
mkdir /launcher-temp && \
|
||||
cd /launcher-temp && \
|
||||
wget https://github.com/n8n-io/task-runner-launcher/releases/download/${LAUNCHER_VERSION}/task-runner-launcher-$ARCH_NAME-unknown-linux-musl.zip && \
|
||||
wget https://github.com/n8n-io/task-runner-launcher/releases/download/${LAUNCHER_VERSION}/task-runner-launcher-$ARCH_NAME-unknown-linux-musl.sha256 && \
|
||||
sha256sum -c task-runner-launcher-$ARCH_NAME-unknown-linux-musl.sha256 && \
|
||||
unzip -d $(dirname ${N8N_RUNNERS_LAUNCHER_PATH}) task-runner-launcher-$ARCH_NAME-unknown-linux-musl.zip task-runner-launcher && \
|
||||
cd - && \
|
||||
rm -r /launcher-temp && \
|
||||
chmod 4555 ${N8N_RUNNERS_LAUNCHER_PATH} && \
|
||||
addgroup -g 2000 task-runner && \
|
||||
adduser -D -u 2000 -g "Task Runner User" -G task-runner task-runner
|
||||
|
||||
COPY docker-entrypoint.sh /
|
||||
|
||||
RUN \
|
||||
|
|
22
docker/images/n8n/n8n-task-runners.json
Normal file
22
docker/images/n8n/n8n-task-runners.json
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"task-runners": [
|
||||
{
|
||||
"runner-type": "javascript",
|
||||
"workdir": "/home/task-runner",
|
||||
"command": "/usr/local/bin/node",
|
||||
"args": ["/usr/local/lib/node_modules/n8n/node_modules/@n8n/task-runner/dist/start.js"],
|
||||
"allowed-env": [
|
||||
"PATH",
|
||||
"N8N_RUNNERS_GRANT_TOKEN",
|
||||
"N8N_RUNNERS_N8N_URI",
|
||||
"N8N_RUNNERS_MAX_PAYLOAD",
|
||||
"N8N_RUNNERS_MAX_CONCURRENCY",
|
||||
"NODE_FUNCTION_ALLOW_BUILTIN",
|
||||
"NODE_FUNCTION_ALLOW_EXTERNAL",
|
||||
"NODE_OPTIONS"
|
||||
],
|
||||
"uid": 2000,
|
||||
"gid": 2000
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-monorepo",
|
||||
"version": "1.63.0",
|
||||
"version": "1.67.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=20.15",
|
||||
|
@ -43,7 +43,9 @@
|
|||
"@biomejs/biome": "^1.9.0",
|
||||
"@n8n_io/eslint-config": "workspace:*",
|
||||
"@types/jest": "^29.5.3",
|
||||
"@types/node": "*",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"jest": "^29.6.2",
|
||||
"jest-environment-jsdom": "^29.6.2",
|
||||
"jest-expect-message": "^1.1.3",
|
||||
|
@ -57,8 +59,8 @@
|
|||
"run-script-os": "^1.0.7",
|
||||
"supertest": "^7.0.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"tsc-alias": "^1.8.7",
|
||||
"tsc-watch": "^6.0.4",
|
||||
"tsc-alias": "^1.8.10",
|
||||
"tsc-watch": "^6.2.0",
|
||||
"turbo": "2.1.2",
|
||||
"typescript": "*",
|
||||
"zx": "^8.1.4"
|
||||
|
@ -82,7 +84,6 @@
|
|||
},
|
||||
"patchedDependencies": {
|
||||
"typedi@0.10.0": "patches/typedi@0.10.0.patch",
|
||||
"@sentry/cli@2.36.2": "patches/@sentry__cli@2.36.2.patch",
|
||||
"pkce-challenge@3.0.0": "patches/pkce-challenge@3.0.0.patch",
|
||||
"pyodide@0.23.4": "patches/pyodide@0.23.4.patch",
|
||||
"@types/express-serve-static-core@4.17.43": "patches/@types__express-serve-static-core@4.17.43.patch",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/api-types",
|
||||
"version": "0.4.0",
|
||||
"version": "0.6.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
@ -21,6 +21,7 @@
|
|||
"dist/**/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@n8n/config": "workspace:*",
|
||||
"n8n-workflow": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import type { FrontendBetaFeatures } from '@n8n/config';
|
||||
import type { ExpressionEvaluatorType, LogLevel, WorkflowSettings } from 'n8n-workflow';
|
||||
|
||||
export interface IVersionNotificationSettings {
|
||||
|
@ -169,4 +170,5 @@ export interface FrontendSettings {
|
|||
security: {
|
||||
blockFileAccessToN8nFiles: boolean;
|
||||
};
|
||||
betaFeatures: FrontendBetaFeatures[];
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# n8n benchmarking tool
|
||||
|
||||
Tool for executing benchmarks against an n8n instance. The tool consists of these components:
|
||||
Tool for executing benchmarks against an n8n instance.
|
||||
|
||||
## Directory structure
|
||||
|
||||
|
@ -12,6 +12,39 @@ packages/@n8n/benchmark
|
|||
├── scripts Orchestration scripts
|
||||
```
|
||||
|
||||
## Benchmarking an existing n8n instance
|
||||
|
||||
The easiest way to run the existing benchmark scenarios is to use the benchmark docker image:
|
||||
|
||||
```sh
|
||||
docker pull ghcr.io/n8n-io/n8n-benchmark:latest
|
||||
# Print the help to list all available flags
|
||||
docker run ghcr.io/n8n-io/n8n-benchmark:latest run --help
|
||||
# Run all available benchmark scenarios for 1 minute with 5 concurrent requests
|
||||
docker run ghcr.io/n8n-io/n8n-benchmark:latest run \
|
||||
--n8nBaseUrl=https://instance.url \
|
||||
--n8nUserEmail=InstanceOwner@email.com \
|
||||
--n8nUserPassword=InstanceOwnerPassword \
|
||||
--vus=5 \
|
||||
--duration=1m \
|
||||
--scenarioFilter SingleWebhook
|
||||
```
|
||||
|
||||
### Using custom scenarios with the Docker image
|
||||
|
||||
It is also possible to create your own [benchmark scenarios](#benchmark-scenarios) and load them using the `--testScenariosPath` flag:
|
||||
|
||||
```sh
|
||||
# Assuming your scenarios are located in `./scenarios`, mount them into `/scenarios` in the container
|
||||
docker run -v ./scenarios:/scenarios ghcr.io/n8n-io/n8n-benchmark:latest run \
|
||||
--n8nBaseUrl=https://instance.url \
|
||||
--n8nUserEmail=InstanceOwner@email.com \
|
||||
--n8nUserPassword=InstanceOwnerPassword \
|
||||
--vus=5 \
|
||||
--duration=1m \
|
||||
--testScenariosPath=/scenarios
|
||||
```
|
||||
|
||||
## Running the entire benchmark suite
|
||||
|
||||
The benchmark suite consists of [benchmark scenarios](#benchmark-scenarios) and different [n8n setups](#n8n-setups).
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/n8n-benchmark",
|
||||
"version": "1.7.0",
|
||||
"version": "1.8.0",
|
||||
"description": "Cli for running benchmark tests for n8n",
|
||||
"main": "dist/index",
|
||||
"scripts": {
|
||||
|
@ -17,7 +17,7 @@
|
|||
"benchmark-locally": "pnpm benchmark --env local",
|
||||
"provision-cloud-env": "zx scripts/provision-cloud-env.mjs",
|
||||
"destroy-cloud-env": "zx scripts/destroy-cloud-env.mjs",
|
||||
"watch": "concurrently \"tsc -w -p tsconfig.build.json\" \"tsc-alias -w -p tsconfig.build.json\""
|
||||
"watch": "tsc-watch -p tsconfig.build.json --onCompilationComplete \"tsc-alias -p tsconfig.build.json\""
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.10"
|
||||
|
@ -41,10 +41,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/convict": "^6.1.1",
|
||||
"@types/k6": "^0.52.0",
|
||||
"@types/node": "^20.14.8",
|
||||
"tsc-alias": "^1.8.7",
|
||||
"typescript": "^5.5.2"
|
||||
"@types/k6": "^0.52.0"
|
||||
},
|
||||
"bin": {
|
||||
"n8n-benchmark": "./bin/n8n-benchmark"
|
||||
|
|
|
@ -15,6 +15,12 @@ export default function () {
|
|||
|
||||
const res = http.post(`${apiBaseUrl}/webhook/binary-files-benchmark`, data);
|
||||
|
||||
if (res.status !== 200) {
|
||||
console.error(
|
||||
`Invalid response. Received status ${res.status}. Body: ${JSON.stringify(res.body)}`,
|
||||
);
|
||||
}
|
||||
|
||||
check(res, {
|
||||
'is status 200': (r) => r.status === 200,
|
||||
'has correct content type': (r) =>
|
||||
|
|
|
@ -6,6 +6,12 @@ const apiBaseUrl = __ENV.API_BASE_URL;
|
|||
export default function () {
|
||||
const res = http.post(`${apiBaseUrl}/webhook/benchmark-http-node`);
|
||||
|
||||
if (res.status !== 200) {
|
||||
console.error(
|
||||
`Invalid response. Received status ${res.status}. Body: ${JSON.stringify(res.body)}`,
|
||||
);
|
||||
}
|
||||
|
||||
check(res, {
|
||||
'is status 200': (r) => r.status === 200,
|
||||
'http requests were OK': (r) => {
|
||||
|
|
|
@ -5,6 +5,13 @@ const apiBaseUrl = __ENV.API_BASE_URL;
|
|||
|
||||
export default function () {
|
||||
const res = http.post(`${apiBaseUrl}/webhook/code-node-benchmark`, {});
|
||||
|
||||
if (res.status !== 200) {
|
||||
console.error(
|
||||
`Invalid response. Received status ${res.status}. Body: ${JSON.stringify(res.body)}`,
|
||||
);
|
||||
}
|
||||
|
||||
check(res, {
|
||||
'is status 200': (r) => r.status === 200,
|
||||
'has items in response': (r) => {
|
||||
|
|
|
@ -5,6 +5,13 @@ const apiBaseUrl = __ENV.API_BASE_URL;
|
|||
|
||||
export default function () {
|
||||
const res = http.post(`${apiBaseUrl}/webhook/set-expressions-benchmark`, {});
|
||||
|
||||
if (res.status !== 200) {
|
||||
console.error(
|
||||
`Invalid response. Received status ${res.status}. Body: ${JSON.stringify(res.body)}`,
|
||||
);
|
||||
}
|
||||
|
||||
check(res, {
|
||||
'is status 200': (r) => r.status === 200,
|
||||
});
|
||||
|
|
|
@ -3,5 +3,5 @@
|
|||
"name": "SingleWebhook",
|
||||
"description": "A single webhook trigger that responds with a 200 status code",
|
||||
"scenarioData": { "workflowFiles": ["single-webhook.json"] },
|
||||
"scriptPath": "single-webhook.script.ts"
|
||||
"scriptPath": "single-webhook.script.js"
|
||||
}
|
||||
|
|
|
@ -5,6 +5,13 @@ const apiBaseUrl = __ENV.API_BASE_URL;
|
|||
|
||||
export default function () {
|
||||
const res = http.get(`${apiBaseUrl}/webhook/single-webhook`);
|
||||
|
||||
if (res.status !== 200) {
|
||||
console.error(
|
||||
`Invalid response. Received status ${res.status}. Body: ${JSON.stringify(res.body)}`,
|
||||
);
|
||||
}
|
||||
|
||||
check(res, {
|
||||
'is status 200': (r) => r.status === 200,
|
||||
});
|
|
@ -176,7 +176,7 @@ services:
|
|||
|
||||
# Load balancer that acts as an entry point for n8n
|
||||
n8n:
|
||||
image: nginx:latest
|
||||
image: nginx:1.27.2
|
||||
ports:
|
||||
- '5678:80'
|
||||
volumes:
|
||||
|
|
|
@ -3,6 +3,7 @@ events {}
|
|||
http {
|
||||
client_max_body_size 50M;
|
||||
access_log off;
|
||||
error_log /dev/stderr warn;
|
||||
|
||||
upstream backend {
|
||||
server n8n_main1:5678;
|
||||
|
|
|
@ -78,12 +78,6 @@ async function runBenchmarksOnVm(config, benchmarkEnv) {
|
|||
const bootstrapScriptPath = path.join(scriptsDir, 'bootstrap.sh');
|
||||
await sshClient.ssh(`chmod a+x ${bootstrapScriptPath} && ${bootstrapScriptPath}`);
|
||||
|
||||
// Benchmarking the VM
|
||||
const vmBenchmarkScriptPath = path.join(scriptsDir, 'vm-benchmark.sh');
|
||||
await sshClient.ssh(`chmod a+x ${vmBenchmarkScriptPath} && ${vmBenchmarkScriptPath}`, {
|
||||
verbose: true,
|
||||
});
|
||||
|
||||
// Give some time for the VM to be ready
|
||||
await sleep(1000);
|
||||
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Install fio
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt-get -y install fio > /dev/null
|
||||
|
||||
# Run the disk benchmark
|
||||
fio --name=rand_rw --ioengine=libaio --rw=randrw --rwmixread=70 --bs=4k --numjobs=4 --size=1G --runtime=30 --directory=/n8n --group_reporting
|
||||
|
||||
# Remove files
|
||||
sudo rm /n8n/rand_rw.*
|
||||
|
||||
# Uninstall fio
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt-get -y remove fio > /dev/null
|
|
@ -13,6 +13,10 @@ export default class RunCommand extends Command {
|
|||
|
||||
static flags = {
|
||||
testScenariosPath,
|
||||
scenarioFilter: Flags.string({
|
||||
char: 'f',
|
||||
description: 'Filter scenarios by name',
|
||||
}),
|
||||
scenarioNamePrefix: Flags.string({
|
||||
description: 'Prefix for the scenario name',
|
||||
default: 'Unnamed',
|
||||
|
@ -95,7 +99,7 @@ export default class RunCommand extends Command {
|
|||
flags.scenarioNamePrefix,
|
||||
);
|
||||
|
||||
const allScenarios = scenarioLoader.loadAll(flags.testScenariosPath);
|
||||
const allScenarios = scenarioLoader.loadAll(flags.testScenariosPath, flags.scenarioFilter);
|
||||
|
||||
await scenarioRunner.runManyScenarios(allScenarios);
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ export class ScenarioLoader {
|
|||
/**
|
||||
* Loads all scenarios from the given path
|
||||
*/
|
||||
loadAll(pathToScenarios: string): Scenario[] {
|
||||
loadAll(pathToScenarios: string, filter?: string): Scenario[] {
|
||||
pathToScenarios = path.resolve(pathToScenarios);
|
||||
const scenarioFolders = fs
|
||||
.readdirSync(pathToScenarios, { withFileTypes: true })
|
||||
|
@ -18,6 +18,9 @@ export class ScenarioLoader {
|
|||
const scenarios: Scenario[] = [];
|
||||
|
||||
for (const folder of scenarioFolders) {
|
||||
if (filter && !folder.toLowerCase().includes(filter.toLowerCase())) {
|
||||
continue;
|
||||
}
|
||||
const scenarioPath = path.join(pathToScenarios, folder);
|
||||
const manifestFileName = `${folder}.manifest.json`;
|
||||
const scenarioManifestPath = path.join(pathToScenarios, folder, manifestFileName);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { sleep } from 'zx';
|
||||
|
||||
import { AuthenticatedN8nApiClient } from '@/n8n-api-client/authenticated-n8n-api-client';
|
||||
import type { N8nApiClient } from '@/n8n-api-client/n8n-api-client';
|
||||
import type { ScenarioDataFileLoader } from '@/scenario/scenario-data-loader';
|
||||
|
@ -47,6 +49,10 @@ export class ScenarioRunner {
|
|||
const testData = await this.dataLoader.loadDataForScenario(scenario);
|
||||
await testDataImporter.importTestScenarioData(testData.workflows);
|
||||
|
||||
// Wait for 1s before executing the scenario to ensure that the workflows are activated.
|
||||
// In multi-main mode it can take some time before the workflow becomes active.
|
||||
await sleep(1000);
|
||||
|
||||
console.log('Executing scenario script');
|
||||
await this.k6Executor.executeTestScenario(scenario, {
|
||||
scenarioRunName,
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"name": "@n8n/chat",
|
||||
"version": "0.28.0",
|
||||
"version": "0.30.0",
|
||||
"scripts": {
|
||||
"dev": "pnpm run storybook",
|
||||
"build": "pnpm build:vite && pnpm build:bundle",
|
||||
"build:vite": "vite build",
|
||||
"build:bundle": "INCLUDE_VUE=true vite build",
|
||||
"build:vite": "cross-env vite build",
|
||||
"build:bundle": "cross-env INCLUDE_VUE=true vite build",
|
||||
"preview": "vite preview",
|
||||
"test:dev": "vitest",
|
||||
"test": "vitest run",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/config",
|
||||
"version": "1.13.0",
|
||||
"version": "1.17.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
|
11
packages/@n8n/config/src/configs/frontend.config.ts
Normal file
11
packages/@n8n/config/src/configs/frontend.config.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { Config, Env } from '../decorators';
|
||||
import { StringArray } from '../utils';
|
||||
|
||||
export type FrontendBetaFeatures = 'canvas_v2';
|
||||
|
||||
@Config
|
||||
export class FrontendConfig {
|
||||
/** Which UI experiments to enable. Separate multiple values with a comma `,` */
|
||||
@Env('N8N_UI_BETA_FEATURES')
|
||||
betaFeatures: StringArray<FrontendBetaFeatures> = [];
|
||||
}
|
15
packages/@n8n/config/src/configs/generic.config.ts
Normal file
15
packages/@n8n/config/src/configs/generic.config.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { Config, Env } from '../decorators';
|
||||
|
||||
@Config
|
||||
export class GenericConfig {
|
||||
/** Default timezone for the n8n instance. Can be overridden on a per-workflow basis. */
|
||||
@Env('GENERIC_TIMEZONE')
|
||||
timezone: string = 'America/New_York';
|
||||
|
||||
@Env('N8N_RELEASE_TYPE')
|
||||
releaseChannel: 'stable' | 'beta' | 'nightly' | 'dev' = 'dev';
|
||||
|
||||
/** Grace period (in seconds) to wait for components to shut down before process exit. */
|
||||
@Env('N8N_GRACEFUL_SHUTDOWN_TIMEOUT')
|
||||
gracefulShutdownTimeout: number = 30;
|
||||
}
|
28
packages/@n8n/config/src/configs/license.config.ts
Normal file
28
packages/@n8n/config/src/configs/license.config.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { Config, Env } from '../decorators';
|
||||
|
||||
@Config
|
||||
export class LicenseConfig {
|
||||
/** License server URL to retrieve license. */
|
||||
@Env('N8N_LICENSE_SERVER_URL')
|
||||
serverUrl: string = 'https://license.n8n.io/v1';
|
||||
|
||||
/** Whether autorenewal for licenses is enabled. */
|
||||
@Env('N8N_LICENSE_AUTO_RENEW_ENABLED')
|
||||
autoRenewalEnabled: boolean = true;
|
||||
|
||||
/** How long (in seconds) before expiry a license should be autorenewed. */
|
||||
@Env('N8N_LICENSE_AUTO_RENEW_OFFSET')
|
||||
autoRenewOffset: number = 60 * 60 * 72; // 72 hours
|
||||
|
||||
/** Activation key to initialize license. */
|
||||
@Env('N8N_LICENSE_ACTIVATION_KEY')
|
||||
activationKey: string = '';
|
||||
|
||||
/** Tenant ID used by the license manager SDK, e.g. for self-hosted, sandbox, embed, cloud. */
|
||||
@Env('N8N_LICENSE_TENANT_ID')
|
||||
tenantId: number = 1;
|
||||
|
||||
/** Ephemeral license certificate. See: https://github.com/n8n-io/license-management?tab=readme-ov-file#concept-ephemeral-entitlements */
|
||||
@Env('N8N_LICENSE_CERT')
|
||||
cert: string = '';
|
||||
}
|
|
@ -1,14 +1,19 @@
|
|||
import { Config, Env, Nested } from '../decorators';
|
||||
import { StringArray } from '../utils';
|
||||
|
||||
/**
|
||||
* Scopes (areas of functionality) to filter logs by.
|
||||
*
|
||||
* `executions` -> execution lifecycle
|
||||
* `license` -> license SDK
|
||||
* `scaling` -> scaling mode
|
||||
*/
|
||||
export const LOG_SCOPES = ['executions', 'license', 'scaling'] as const;
|
||||
/** Scopes (areas of functionality) to filter logs by. */
|
||||
export const LOG_SCOPES = [
|
||||
'concurrency',
|
||||
'external-secrets',
|
||||
'license',
|
||||
'multi-main-setup',
|
||||
'pruning',
|
||||
'pubsub',
|
||||
'redis',
|
||||
'scaling',
|
||||
'waiting-executions',
|
||||
'task-runner',
|
||||
] as const;
|
||||
|
||||
export type LogScope = (typeof LOG_SCOPES)[number];
|
||||
|
||||
|
@ -59,14 +64,20 @@ export class LoggingConfig {
|
|||
/**
|
||||
* Scopes to filter logs by. Nothing is filtered by default.
|
||||
*
|
||||
* Currently supported log scopes:
|
||||
* - `executions`
|
||||
* Supported log scopes:
|
||||
*
|
||||
* - `concurrency`
|
||||
* - `external-secrets`
|
||||
* - `license`
|
||||
* - `multi-main-setup`
|
||||
* - `pubsub`
|
||||
* - `redis`
|
||||
* - `scaling`
|
||||
* - `waiting-executions`
|
||||
*
|
||||
* @example
|
||||
* `N8N_LOG_SCOPES=license`
|
||||
* `N8N_LOG_SCOPES=license,executions`
|
||||
* `N8N_LOG_SCOPES=license,waiting-executions`
|
||||
*/
|
||||
@Env('N8N_LOG_SCOPES')
|
||||
scopes: StringArray<LogScope> = [];
|
||||
|
|
16
packages/@n8n/config/src/configs/multi-main-setup.config.ts
Normal file
16
packages/@n8n/config/src/configs/multi-main-setup.config.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { Config, Env } from '../decorators';
|
||||
|
||||
@Config
|
||||
export class MultiMainSetupConfig {
|
||||
/** Whether to enable multi-main setup (if licensed) for scaling mode. */
|
||||
@Env('N8N_MULTI_MAIN_SETUP_ENABLED')
|
||||
enabled: boolean = false;
|
||||
|
||||
/** Time to live (in seconds) for leader key in multi-main setup. */
|
||||
@Env('N8N_MULTI_MAIN_SETUP_KEY_TTL')
|
||||
ttl: number = 10;
|
||||
|
||||
/** Interval (in seconds) for leader check in multi-main setup. */
|
||||
@Env('N8N_MULTI_MAIN_SETUP_CHECK_INTERVAL')
|
||||
interval: number = 3;
|
||||
}
|
35
packages/@n8n/config/src/configs/pruning.config.ts
Normal file
35
packages/@n8n/config/src/configs/pruning.config.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { Config, Env } from '../decorators';
|
||||
|
||||
@Config
|
||||
export class PruningConfig {
|
||||
/** Whether to delete past executions on a rolling basis. */
|
||||
@Env('EXECUTIONS_DATA_PRUNE')
|
||||
isEnabled: boolean = true;
|
||||
|
||||
/** How old (hours) a finished execution must be to qualify for soft-deletion. */
|
||||
@Env('EXECUTIONS_DATA_MAX_AGE')
|
||||
maxAge: number = 336;
|
||||
|
||||
/**
|
||||
* Max number of finished executions to keep in database. Does not necessarily
|
||||
* prune to the exact max number. `0` for unlimited.
|
||||
*/
|
||||
@Env('EXECUTIONS_DATA_PRUNE_MAX_COUNT')
|
||||
maxCount: number = 10_000;
|
||||
|
||||
/**
|
||||
* How old (hours) a finished execution must be to qualify for hard-deletion.
|
||||
* This buffer by default excludes recent executions as the user may need
|
||||
* them while building a workflow.
|
||||
*/
|
||||
@Env('EXECUTIONS_DATA_HARD_DELETE_BUFFER')
|
||||
hardDeleteBuffer: number = 1;
|
||||
|
||||
/** How often (minutes) execution data should be hard-deleted. */
|
||||
@Env('EXECUTIONS_DATA_PRUNE_HARD_DELETE_INTERVAL')
|
||||
hardDeleteInterval: number = 15;
|
||||
|
||||
/** How often (minutes) execution data should be soft-deleted */
|
||||
@Env('EXECUTIONS_DATA_PRUNE_SOFT_DELETE_INTERVAL')
|
||||
softDeleteInterval: number = 60;
|
||||
}
|
|
@ -1,10 +1,21 @@
|
|||
import { Config, Env } from '../decorators';
|
||||
|
||||
/**
|
||||
* Whether to enable task runners and how to run them
|
||||
* - internal_childprocess: Task runners are run as a child process and launched by n8n
|
||||
* - internal_launcher: Task runners are run as a child process and launched by n8n using a separate launch program
|
||||
* - external: Task runners are run as a separate program not launched by n8n
|
||||
*/
|
||||
export type TaskRunnerMode = 'internal_childprocess' | 'internal_launcher' | 'external';
|
||||
|
||||
@Config
|
||||
export class TaskRunnersConfig {
|
||||
@Env('N8N_RUNNERS_ENABLED')
|
||||
enabled: boolean = false;
|
||||
|
||||
// Defaults to true for now
|
||||
@Env('N8N_RUNNERS_DISABLED')
|
||||
disabled: boolean = true;
|
||||
@Env('N8N_RUNNERS_MODE')
|
||||
mode: TaskRunnerMode = 'internal_childprocess';
|
||||
|
||||
@Env('N8N_RUNNERS_PATH')
|
||||
path: string = '/runners';
|
||||
|
@ -18,5 +29,28 @@ export class TaskRunnersConfig {
|
|||
|
||||
/** IP address task runners server should listen on */
|
||||
@Env('N8N_RUNNERS_SERVER_LISTEN_ADDRESS')
|
||||
listen_address: string = '127.0.0.1';
|
||||
listenAddress: string = '127.0.0.1';
|
||||
|
||||
/** Maximum size of a payload sent to the runner in bytes, Default 1G */
|
||||
@Env('N8N_RUNNERS_MAX_PAYLOAD')
|
||||
maxPayload: number = 1024 * 1024 * 1024;
|
||||
|
||||
@Env('N8N_RUNNERS_LAUNCHER_PATH')
|
||||
launcherPath: string = '';
|
||||
|
||||
/** Which task runner to launch from the config */
|
||||
@Env('N8N_RUNNERS_LAUNCHER_RUNNER')
|
||||
launcherRunner: string = 'javascript';
|
||||
|
||||
/** The --max-old-space-size option to use for the runner (in MB). Default means node.js will determine it based on the available memory. */
|
||||
@Env('N8N_RUNNERS_MAX_OLD_SPACE_SIZE')
|
||||
maxOldSpaceSize: string = '';
|
||||
|
||||
/** How many concurrent tasks can a runner execute at a time */
|
||||
@Env('N8N_RUNNERS_MAX_CONCURRENCY')
|
||||
maxConcurrency: number = 5;
|
||||
|
||||
/** Should the output of deduplication be asserted for correctness */
|
||||
@Env('N8N_RUNNERS_ASSERT_DEDUPLICATION_OUTPUT')
|
||||
assertDeduplicationOutput: boolean = false;
|
||||
}
|
||||
|
|
|
@ -82,10 +82,6 @@ class BullConfig {
|
|||
@Nested
|
||||
redis: RedisConfig;
|
||||
|
||||
/** How often (in seconds) to poll the Bull queue to identify executions finished during a Redis crash. `0` to disable. May increase Redis traffic significantly. */
|
||||
@Env('QUEUE_RECOVERY_INTERVAL')
|
||||
queueRecoveryInterval: number = 60; // watchdog interval
|
||||
|
||||
/** @deprecated How long (in seconds) a worker must wait for active executions to finish before exiting. Use `N8N_GRACEFUL_SHUTDOWN_TIMEOUT` instead */
|
||||
@Env('QUEUE_WORKER_TIMEOUT')
|
||||
gracefulShutdownTimeout: number = 30;
|
||||
|
|
27
packages/@n8n/config/src/configs/security.config.ts
Normal file
27
packages/@n8n/config/src/configs/security.config.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { Config, Env } from '../decorators';
|
||||
|
||||
@Config
|
||||
export class SecurityConfig {
|
||||
/**
|
||||
* Which directories to limit n8n's access to. Separate multiple dirs with semicolon `;`.
|
||||
*
|
||||
* @example N8N_RESTRICT_FILE_ACCESS_TO=/home/user/.n8n;/home/user/n8n-data
|
||||
*/
|
||||
@Env('N8N_RESTRICT_FILE_ACCESS_TO')
|
||||
restrictFileAccessTo: string = '';
|
||||
|
||||
/**
|
||||
* Whether to block access to all files at:
|
||||
* - the ".n8n" directory,
|
||||
* - the static cache dir at ~/.cache/n8n/public, and
|
||||
* - user-defined config files.
|
||||
*/
|
||||
@Env('N8N_BLOCK_FILE_ACCESS_TO_N8N_FILES')
|
||||
blockFileAccessToN8nFiles: boolean = true;
|
||||
|
||||
/**
|
||||
* In a [security audit](https://docs.n8n.io/hosting/securing/security-audit/), how many days for a workflow to be considered abandoned if not executed.
|
||||
*/
|
||||
@Env('N8N_SECURITY_AUDIT_DAYS_ABANDONED_WORKFLOW')
|
||||
daysAbandonedWorkflow: number = 90;
|
||||
}
|
|
@ -5,12 +5,16 @@ import { EndpointsConfig } from './configs/endpoints.config';
|
|||
import { EventBusConfig } from './configs/event-bus.config';
|
||||
import { ExternalSecretsConfig } from './configs/external-secrets.config';
|
||||
import { ExternalStorageConfig } from './configs/external-storage.config';
|
||||
import { GenericConfig } from './configs/generic.config';
|
||||
import { LicenseConfig } from './configs/license.config';
|
||||
import { LoggingConfig } from './configs/logging.config';
|
||||
import { MultiMainSetupConfig } from './configs/multi-main-setup.config';
|
||||
import { NodesConfig } from './configs/nodes.config';
|
||||
import { PruningConfig } from './configs/pruning.config';
|
||||
import { PublicApiConfig } from './configs/public-api.config';
|
||||
import { TaskRunnersConfig } from './configs/runners.config';
|
||||
export { TaskRunnersConfig } from './configs/runners.config';
|
||||
import { ScalingModeConfig } from './configs/scaling-mode.config';
|
||||
import { SecurityConfig } from './configs/security.config';
|
||||
import { SentryConfig } from './configs/sentry.config';
|
||||
import { TemplatesConfig } from './configs/templates.config';
|
||||
import { UserManagementConfig } from './configs/user-management.config';
|
||||
|
@ -18,6 +22,11 @@ import { VersionNotificationsConfig } from './configs/version-notifications.conf
|
|||
import { WorkflowsConfig } from './configs/workflows.config';
|
||||
import { Config, Env, Nested } from './decorators';
|
||||
|
||||
export { Config, Env, Nested } from './decorators';
|
||||
export { TaskRunnersConfig } from './configs/runners.config';
|
||||
export { SecurityConfig } from './configs/security.config';
|
||||
export { PruningConfig } from './configs/pruning.config';
|
||||
export { FrontendBetaFeatures, FrontendConfig } from './configs/frontend.config';
|
||||
export { LOG_SCOPES } from './configs/logging.config';
|
||||
export type { LogScope } from './configs/logging.config';
|
||||
|
||||
|
@ -93,4 +102,19 @@ export class GlobalConfig {
|
|||
|
||||
@Nested
|
||||
taskRunners: TaskRunnersConfig;
|
||||
|
||||
@Nested
|
||||
multiMainSetup: MultiMainSetupConfig;
|
||||
|
||||
@Nested
|
||||
generic: GenericConfig;
|
||||
|
||||
@Nested
|
||||
license: LicenseConfig;
|
||||
|
||||
@Nested
|
||||
security: SecurityConfig;
|
||||
|
||||
@Nested
|
||||
pruning: PruningConfig;
|
||||
}
|
||||
|
|
|
@ -211,7 +211,6 @@ describe('GlobalConfig', () => {
|
|||
clusterNodes: '',
|
||||
tls: false,
|
||||
},
|
||||
queueRecoveryInterval: 60,
|
||||
gracefulShutdownTimeout: 30,
|
||||
prefix: 'bull',
|
||||
settings: {
|
||||
|
@ -223,11 +222,18 @@ describe('GlobalConfig', () => {
|
|||
},
|
||||
},
|
||||
taskRunners: {
|
||||
disabled: true,
|
||||
enabled: false,
|
||||
mode: 'internal_childprocess',
|
||||
path: '/runners',
|
||||
authToken: '',
|
||||
listen_address: '127.0.0.1',
|
||||
listenAddress: '127.0.0.1',
|
||||
maxPayload: 1024 * 1024 * 1024,
|
||||
port: 5679,
|
||||
launcherPath: '',
|
||||
launcherRunner: 'javascript',
|
||||
maxOldSpaceSize: '',
|
||||
maxConcurrency: 5,
|
||||
assertDeduplicationOutput: false,
|
||||
},
|
||||
sentry: {
|
||||
backendDsn: '',
|
||||
|
@ -243,6 +249,37 @@ describe('GlobalConfig', () => {
|
|||
},
|
||||
scopes: [],
|
||||
},
|
||||
multiMainSetup: {
|
||||
enabled: false,
|
||||
ttl: 10,
|
||||
interval: 3,
|
||||
},
|
||||
generic: {
|
||||
timezone: 'America/New_York',
|
||||
releaseChannel: 'dev',
|
||||
gracefulShutdownTimeout: 30,
|
||||
},
|
||||
license: {
|
||||
serverUrl: 'https://license.n8n.io/v1',
|
||||
autoRenewalEnabled: true,
|
||||
autoRenewOffset: 60 * 60 * 72,
|
||||
activationKey: '',
|
||||
tenantId: 1,
|
||||
cert: '',
|
||||
},
|
||||
security: {
|
||||
restrictFileAccessTo: '',
|
||||
blockFileAccessToN8nFiles: true,
|
||||
daysAbandonedWorkflow: 90,
|
||||
},
|
||||
pruning: {
|
||||
isEnabled: true,
|
||||
maxAge: 336,
|
||||
maxCount: 10_000,
|
||||
hardDeleteBuffer: 1,
|
||||
hardDeleteInterval: 15,
|
||||
softDeleteInterval: 60,
|
||||
},
|
||||
};
|
||||
|
||||
it('should use all default values when no env variables are defined', () => {
|
||||
|
|
21
packages/@n8n/json-schema-to-zod/.eslintrc.js
Normal file
21
packages/@n8n/json-schema-to-zod/.eslintrc.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
const sharedOptions = require('@n8n_io/eslint-config/shared');
|
||||
|
||||
/**
|
||||
* @type {import('@types/eslint').ESLint.ConfigData}
|
||||
*/
|
||||
module.exports = {
|
||||
extends: ['@n8n_io/eslint-config/node'],
|
||||
|
||||
...sharedOptions(__dirname),
|
||||
|
||||
ignorePatterns: ['jest.config.js'],
|
||||
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
'@typescript-eslint/no-duplicate-imports': 'off',
|
||||
'import/no-cycle': 'off',
|
||||
'n8n-local-rules/no-plain-errors': 'off',
|
||||
|
||||
complexity: 'error',
|
||||
},
|
||||
};
|
4
packages/@n8n/json-schema-to-zod/.gitignore
vendored
Normal file
4
packages/@n8n/json-schema-to-zod/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
node_modules
|
||||
dist
|
||||
coverage
|
||||
test/output
|
3
packages/@n8n/json-schema-to-zod/.npmignore
Normal file
3
packages/@n8n/json-schema-to-zod/.npmignore
Normal file
|
@ -0,0 +1,3 @@
|
|||
src
|
||||
tsconfig*
|
||||
test
|
16
packages/@n8n/json-schema-to-zod/LICENSE
Normal file
16
packages/@n8n/json-schema-to-zod/LICENSE
Normal file
|
@ -0,0 +1,16 @@
|
|||
ISC License
|
||||
|
||||
Copyright (c) 2024, n8n
|
||||
Copyright (c) 2021, Stefan Terdell
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
34
packages/@n8n/json-schema-to-zod/README.md
Normal file
34
packages/@n8n/json-schema-to-zod/README.md
Normal file
|
@ -0,0 +1,34 @@
|
|||
# Json-Schema-to-Zod
|
||||
|
||||
A package to convert JSON schema (draft 4+) objects into Zod schemas in the form of Zod objects at runtime.
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
npm install @n8n/json-schema-to-zod
|
||||
```
|
||||
|
||||
### Simple example
|
||||
|
||||
```typescript
|
||||
import { jsonSchemaToZod } from "json-schema-to-zod";
|
||||
|
||||
const jsonSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
hello: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const zodSchema = jsonSchemaToZod(myObject);
|
||||
```
|
||||
|
||||
### Overriding a parser
|
||||
|
||||
You can pass a function to the `overrideParser` option, which represents a function that receives the current schema node and the reference object, and should return a zod object when it wants to replace a default output. If the default output should be used for the node just return undefined.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
This is a fork of [`json-schema-to-zod`](https://github.com/StefanTerdell/json-schema-to-zod).
|
5
packages/@n8n/json-schema-to-zod/jest.config.js
Normal file
5
packages/@n8n/json-schema-to-zod/jest.config.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
/** @type {import('jest').Config} */
|
||||
module.exports = {
|
||||
...require('../../../jest.config'),
|
||||
setupFilesAfterEnv: ['<rootDir>/test/extend-expect.ts'],
|
||||
};
|
68
packages/@n8n/json-schema-to-zod/package.json
Normal file
68
packages/@n8n/json-schema-to-zod/package.json
Normal file
|
@ -0,0 +1,68 @@
|
|||
{
|
||||
"name": "@n8n/json-schema-to-zod",
|
||||
"version": "1.1.0",
|
||||
"description": "Converts JSON schema objects into Zod schemas",
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"main": "./dist/cjs/index.js",
|
||||
"module": "./dist/esm/index.js",
|
||||
"exports": {
|
||||
"import": {
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"default": "./dist/esm/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"default": "./dist/cjs/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "tsc -w",
|
||||
"format": "biome format --write src",
|
||||
"format:check": "biome ci src",
|
||||
"lint": "eslint . --quiet",
|
||||
"lintfix": "eslint . --fix",
|
||||
"build:types": "tsc -p tsconfig.types.json",
|
||||
"build:cjs": "tsc -p tsconfig.cjs.json && node postcjs.js",
|
||||
"build:esm": "tsc -p tsconfig.esm.json && node postesm.js",
|
||||
"build": "rimraf ./dist && pnpm run build:types && pnpm run build:cjs && pnpm run build:esm",
|
||||
"dry": "pnpm run build && pnpm pub --dry-run",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch"
|
||||
},
|
||||
"keywords": [
|
||||
"zod",
|
||||
"json",
|
||||
"schema",
|
||||
"converter",
|
||||
"cli"
|
||||
],
|
||||
"author": "Stefan Terdell",
|
||||
"contributors": [
|
||||
"Chen (https://github.com/werifu)",
|
||||
"Nuno Carduso (https://github.com/ncardoso-barracuda)",
|
||||
"Lars Strojny (https://github.com/lstrojny)",
|
||||
"Navtoj Chahal (https://github.com/navtoj)",
|
||||
"Ben McCann (https://github.com/benmccann)",
|
||||
"Dmitry Zakharov (https://github.com/DZakh)",
|
||||
"Michel Turpin (https://github.com/grimly)",
|
||||
"David Barratt (https://github.com/davidbarratt)",
|
||||
"pevisscher (https://github.com/pevisscher)",
|
||||
"Aidin Abedi (https://github.com/aidinabedi)",
|
||||
"Brett Zamir (https://github.com/brettz9)",
|
||||
"n8n (https://github.com/n8n-io)"
|
||||
],
|
||||
"license": "ISC",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/n8n-io/n8n"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"zod": "catalog:"
|
||||
}
|
||||
}
|
1
packages/@n8n/json-schema-to-zod/postcjs.js
Normal file
1
packages/@n8n/json-schema-to-zod/postcjs.js
Normal file
|
@ -0,0 +1 @@
|
|||
require('fs').writeFileSync('./dist/cjs/package.json', '{"type":"commonjs"}', 'utf-8');
|
1
packages/@n8n/json-schema-to-zod/postesm.js
Normal file
1
packages/@n8n/json-schema-to-zod/postesm.js
Normal file
|
@ -0,0 +1 @@
|
|||
require('fs').writeFileSync('./dist/esm/package.json', '{"type":"module"}', 'utf-8');
|
2
packages/@n8n/json-schema-to-zod/src/index.ts
Normal file
2
packages/@n8n/json-schema-to-zod/src/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export type * from './types';
|
||||
export { jsonSchemaToZod } from './json-schema-to-zod.js';
|
15
packages/@n8n/json-schema-to-zod/src/json-schema-to-zod.ts
Normal file
15
packages/@n8n/json-schema-to-zod/src/json-schema-to-zod.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import type { z } from 'zod';
|
||||
|
||||
import { parseSchema } from './parsers/parse-schema';
|
||||
import type { JsonSchemaToZodOptions, JsonSchema } from './types';
|
||||
|
||||
export const jsonSchemaToZod = <T extends z.ZodTypeAny = z.ZodTypeAny>(
|
||||
schema: JsonSchema,
|
||||
options: JsonSchemaToZodOptions = {},
|
||||
): T => {
|
||||
return parseSchema(schema, {
|
||||
path: [],
|
||||
seen: new Map(),
|
||||
...options,
|
||||
}) as T;
|
||||
};
|
46
packages/@n8n/json-schema-to-zod/src/parsers/parse-all-of.ts
Normal file
46
packages/@n8n/json-schema-to-zod/src/parsers/parse-all-of.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { parseSchema } from './parse-schema';
|
||||
import type { JsonSchemaObject, JsonSchema, Refs } from '../types';
|
||||
import { half } from '../utils/half';
|
||||
|
||||
const originalIndex = Symbol('Original index');
|
||||
|
||||
const ensureOriginalIndex = (arr: JsonSchema[]) => {
|
||||
const newArr = [];
|
||||
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
const item = arr[i];
|
||||
if (typeof item === 'boolean') {
|
||||
newArr.push(item ? { [originalIndex]: i } : { [originalIndex]: i, not: {} });
|
||||
} else if (originalIndex in item) {
|
||||
return arr;
|
||||
} else {
|
||||
newArr.push({ ...item, [originalIndex]: i });
|
||||
}
|
||||
}
|
||||
|
||||
return newArr;
|
||||
};
|
||||
|
||||
export function parseAllOf(
|
||||
jsonSchema: JsonSchemaObject & { allOf: JsonSchema[] },
|
||||
refs: Refs,
|
||||
): z.ZodTypeAny {
|
||||
if (jsonSchema.allOf.length === 0) {
|
||||
return z.never();
|
||||
}
|
||||
|
||||
if (jsonSchema.allOf.length === 1) {
|
||||
const item = jsonSchema.allOf[0];
|
||||
|
||||
return parseSchema(item, {
|
||||
...refs,
|
||||
path: [...refs.path, 'allOf', (item as never)[originalIndex]],
|
||||
});
|
||||
}
|
||||
|
||||
const [left, right] = half(ensureOriginalIndex(jsonSchema.allOf));
|
||||
|
||||
return z.intersection(parseAllOf({ allOf: left }, refs), parseAllOf({ allOf: right }, refs));
|
||||
}
|
19
packages/@n8n/json-schema-to-zod/src/parsers/parse-any-of.ts
Normal file
19
packages/@n8n/json-schema-to-zod/src/parsers/parse-any-of.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { parseSchema } from './parse-schema';
|
||||
import type { JsonSchemaObject, JsonSchema, Refs } from '../types';
|
||||
|
||||
export const parseAnyOf = (jsonSchema: JsonSchemaObject & { anyOf: JsonSchema[] }, refs: Refs) => {
|
||||
return jsonSchema.anyOf.length
|
||||
? jsonSchema.anyOf.length === 1
|
||||
? parseSchema(jsonSchema.anyOf[0], {
|
||||
...refs,
|
||||
path: [...refs.path, 'anyOf', 0],
|
||||
})
|
||||
: z.union(
|
||||
jsonSchema.anyOf.map((schema, i) =>
|
||||
parseSchema(schema, { ...refs, path: [...refs.path, 'anyOf', i] }),
|
||||
) as [z.ZodTypeAny, z.ZodTypeAny],
|
||||
)
|
||||
: z.any();
|
||||
};
|
34
packages/@n8n/json-schema-to-zod/src/parsers/parse-array.ts
Normal file
34
packages/@n8n/json-schema-to-zod/src/parsers/parse-array.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { parseSchema } from './parse-schema';
|
||||
import type { JsonSchemaObject, Refs } from '../types';
|
||||
import { extendSchemaWithMessage } from '../utils/extend-schema';
|
||||
|
||||
export const parseArray = (jsonSchema: JsonSchemaObject & { type: 'array' }, refs: Refs) => {
|
||||
if (Array.isArray(jsonSchema.items)) {
|
||||
return z.tuple(
|
||||
jsonSchema.items.map((v, i) =>
|
||||
parseSchema(v, { ...refs, path: [...refs.path, 'items', i] }),
|
||||
) as [z.ZodTypeAny],
|
||||
);
|
||||
}
|
||||
|
||||
let zodSchema = !jsonSchema.items
|
||||
? z.array(z.any())
|
||||
: z.array(parseSchema(jsonSchema.items, { ...refs, path: [...refs.path, 'items'] }));
|
||||
|
||||
zodSchema = extendSchemaWithMessage(
|
||||
zodSchema,
|
||||
jsonSchema,
|
||||
'minItems',
|
||||
(zs, minItems, errorMessage) => zs.min(minItems, errorMessage),
|
||||
);
|
||||
zodSchema = extendSchemaWithMessage(
|
||||
zodSchema,
|
||||
jsonSchema,
|
||||
'maxItems',
|
||||
(zs, maxItems, errorMessage) => zs.max(maxItems, errorMessage),
|
||||
);
|
||||
|
||||
return zodSchema;
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import type { JsonSchemaObject } from '../types';
|
||||
|
||||
export const parseBoolean = (_jsonSchema: JsonSchemaObject & { type: 'boolean' }) => {
|
||||
return z.boolean();
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import type { JsonSchemaObject, Serializable } from '../types';
|
||||
|
||||
export const parseConst = (jsonSchema: JsonSchemaObject & { const: Serializable }) => {
|
||||
return z.literal(jsonSchema.const as z.Primitive);
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import type { JsonSchemaObject } from '../types';
|
||||
|
||||
export const parseDefault = (_jsonSchema: JsonSchemaObject) => {
|
||||
return z.any();
|
||||
};
|
25
packages/@n8n/json-schema-to-zod/src/parsers/parse-enum.ts
Normal file
25
packages/@n8n/json-schema-to-zod/src/parsers/parse-enum.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import type { JsonSchemaObject, Serializable } from '../types';
|
||||
|
||||
export const parseEnum = (jsonSchema: JsonSchemaObject & { enum: Serializable[] }) => {
|
||||
if (jsonSchema.enum.length === 0) {
|
||||
return z.never();
|
||||
}
|
||||
|
||||
if (jsonSchema.enum.length === 1) {
|
||||
// union does not work when there is only one element
|
||||
return z.literal(jsonSchema.enum[0] as z.Primitive);
|
||||
}
|
||||
|
||||
if (jsonSchema.enum.every((x) => typeof x === 'string')) {
|
||||
return z.enum(jsonSchema.enum as [string]);
|
||||
}
|
||||
|
||||
return z.union(
|
||||
jsonSchema.enum.map((x) => z.literal(x as z.Primitive)) as unknown as [
|
||||
z.ZodTypeAny,
|
||||
z.ZodTypeAny,
|
||||
],
|
||||
);
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { parseSchema } from './parse-schema';
|
||||
import type { JsonSchemaObject, JsonSchema, Refs } from '../types';
|
||||
|
||||
export const parseIfThenElse = (
|
||||
jsonSchema: JsonSchemaObject & {
|
||||
if: JsonSchema;
|
||||
then: JsonSchema;
|
||||
else: JsonSchema;
|
||||
},
|
||||
refs: Refs,
|
||||
) => {
|
||||
const $if = parseSchema(jsonSchema.if, { ...refs, path: [...refs.path, 'if'] });
|
||||
const $then = parseSchema(jsonSchema.then, {
|
||||
...refs,
|
||||
path: [...refs.path, 'then'],
|
||||
});
|
||||
const $else = parseSchema(jsonSchema.else, {
|
||||
...refs,
|
||||
path: [...refs.path, 'else'],
|
||||
});
|
||||
|
||||
return z.union([$then, $else]).superRefine((value, ctx) => {
|
||||
const result = $if.safeParse(value).success ? $then.safeParse(value) : $else.safeParse(value);
|
||||
|
||||
if (!result.success) {
|
||||
result.error.errors.forEach((error) => ctx.addIssue(error));
|
||||
}
|
||||
});
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { parseSchema } from './parse-schema';
|
||||
import type { JsonSchema, JsonSchemaObject, Refs } from '../types';
|
||||
|
||||
export const parseMultipleType = (
|
||||
jsonSchema: JsonSchemaObject & { type: string[] },
|
||||
refs: Refs,
|
||||
) => {
|
||||
return z.union(
|
||||
jsonSchema.type.map((type) => parseSchema({ ...jsonSchema, type } as JsonSchema, refs)) as [
|
||||
z.ZodTypeAny,
|
||||
z.ZodTypeAny,
|
||||
],
|
||||
);
|
||||
};
|
15
packages/@n8n/json-schema-to-zod/src/parsers/parse-not.ts
Normal file
15
packages/@n8n/json-schema-to-zod/src/parsers/parse-not.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { parseSchema } from './parse-schema';
|
||||
import type { JsonSchemaObject, JsonSchema, Refs } from '../types';
|
||||
|
||||
export const parseNot = (jsonSchema: JsonSchemaObject & { not: JsonSchema }, refs: Refs) => {
|
||||
return z.any().refine(
|
||||
(value) =>
|
||||
!parseSchema(jsonSchema.not, {
|
||||
...refs,
|
||||
path: [...refs.path, 'not'],
|
||||
}).safeParse(value).success,
|
||||
'Invalid input: Should NOT be valid against schema',
|
||||
);
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import type { JsonSchemaObject } from '../types';
|
||||
|
||||
export const parseNull = (_jsonSchema: JsonSchemaObject & { type: 'null' }) => {
|
||||
return z.null();
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
import { parseSchema } from './parse-schema';
|
||||
import type { JsonSchemaObject, Refs } from '../types';
|
||||
import { omit } from '../utils/omit';
|
||||
|
||||
/**
|
||||
* For compatibility with open api 3.0 nullable
|
||||
*/
|
||||
export const parseNullable = (jsonSchema: JsonSchemaObject & { nullable: true }, refs: Refs) => {
|
||||
return parseSchema(omit(jsonSchema, 'nullable'), refs, true).nullable();
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue