mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-09 22:24:05 -08:00
Merge branch 'n8n-io:master' into feature/support-add-labels-for-gitlab-issue-api
This commit is contained in:
commit
a1e177b021
72
.github/pull_request_title_conventions.md
vendored
72
.github/pull_request_title_conventions.md
vendored
|
@ -4,75 +4,79 @@ We have very precise rules over how Pull Requests (to the `master` branch) must
|
|||
|
||||
A PR title consists of these elements:
|
||||
|
||||
```
|
||||
```text
|
||||
<type>(<scope>): <summary>
|
||||
│ │ │
|
||||
│ │ └─⫸ Summary: In imperative present tense.
|
||||
| | Capitalized
|
||||
| | No period at the end.
|
||||
│ │
|
||||
│ └─⫸ Scope: API|core|editor|* Node
|
||||
│ └─⫸ Scope: API | benchmark | core | editor | * Node
|
||||
│
|
||||
└─⫸ Type: build|ci|docs|feat|fix|perf|refactor|test
|
||||
└─⫸ Type: build | ci | chore | docs | feat | fix | perf | refactor | test
|
||||
```
|
||||
|
||||
- PR title
|
||||
- type
|
||||
- scope (*optional*)
|
||||
- summary
|
||||
- type
|
||||
- scope (_optional_)
|
||||
- summary
|
||||
- PR description
|
||||
- body (optional)
|
||||
- blank line
|
||||
- footer (optional)
|
||||
- body (optional)
|
||||
- blank line
|
||||
- footer (optional)
|
||||
|
||||
The structure looks like this:
|
||||
|
||||
### **Type**
|
||||
## Type
|
||||
|
||||
Must be one of the following:
|
||||
|
||||
- `feat` - A new feature
|
||||
- `fix` - A bug fix
|
||||
- `perf` - A code change that improves performance
|
||||
- `test` - Adding missing tests or correcting existing tests
|
||||
- `docs` - Documentation only changes
|
||||
- `refactor` - A code change that neither fixes a bug nor adds a feature
|
||||
- `build` - Changes that affect the build system or external dependencies (example scopes: broccoli, npm)
|
||||
- `ci` - Changes to our CI configuration files and scripts (e.g. Github actions)
|
||||
| type | description | appears in changelog |
|
||||
| --- | --- | --- |
|
||||
| `feat` | A new feature | ✅ |
|
||||
| `fix` | A bug fix | ✅ |
|
||||
| `perf` | A code change that improves performance | ✅ |
|
||||
| `test` | Adding missing tests or correcting existing tests | ❌ |
|
||||
| `docs` | Documentation only changes | ❌ |
|
||||
| `refactor` | A behavior-neutral code change that neither fixes a bug nor adds a feature | ❌ |
|
||||
| `build` | Changes that affect the build system or external dependencies (TypeScript, Jest, pnpm, etc.) | ❌ |
|
||||
| `ci` | Changes to CI configuration files and scripts (e.g. Github actions) | ❌ |
|
||||
| `chore` | Routine tasks, maintenance, and minor updates not covered by other types | ❌ |
|
||||
|
||||
If the prefix is `feat`, `fix` or `perf`, it will appear in the changelog. However if there is any BREAKING CHANGE (see Footer section below), the commit will always appear in the changelog.
|
||||
> BREAKING CHANGES (see Footer section below), will **always** appear in the changelog unless suffixed with `no-changelog`.
|
||||
|
||||
### **Scope (optional)**
|
||||
## Scope (optional)
|
||||
|
||||
The scope should specify the place of the commit change as long as the commit clearly addresses one of the following supported scopes. (Otherwise, omit the scope!)
|
||||
|
||||
- `API` - changes to the *public* API
|
||||
- `API` - changes to the _public_ API
|
||||
- `benchmark` - changes to the benchmark cli
|
||||
- `core` - changes to the core / private API / backend of n8n
|
||||
- `editor` - changes to the Editor UI
|
||||
- `* Node` - changes to a specific node or trigger node (”`*`” to be replaced with the node name, not its display name), e.g.
|
||||
- mattermost → Mattermost Node
|
||||
- microsoftToDo → Microsoft To Do Node
|
||||
- n8n → n8n Node
|
||||
- mattermost → Mattermost Node
|
||||
- microsoftToDo → Microsoft To Do Node
|
||||
- n8n → n8n Node
|
||||
|
||||
### **Summary**
|
||||
## Summary
|
||||
|
||||
The summary contains succinct description of the change:
|
||||
|
||||
- use the imperative, present tense: "change" not "changed" nor "changes"
|
||||
- capitalize the first letter
|
||||
- *no* dot (.) at the end
|
||||
- do *not* include Linear ticket IDs etc. (e.g. N8N-1234)
|
||||
- _no_ dot (.) at the end
|
||||
- do _not_ include Linear ticket IDs etc. (e.g. N8N-1234)
|
||||
- suffix with “(no-changelog)” for commits / PRs that should not get mentioned in the changelog.
|
||||
|
||||
### **Body (optional)**
|
||||
## Body (optional)
|
||||
|
||||
Just as in the **summary**, use the imperative, present tense: "change" not "changed" nor "changes". The body should include the motivation for the change and contrast this with previous behavior.
|
||||
|
||||
### **Footer (optional)**
|
||||
## Footer (optional)
|
||||
|
||||
The footer can contain information about breaking changes and deprecations and is also the place to [reference GitHub issues](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword), Linear tickets, and other PRs that this commit closes or is related to. For example:
|
||||
|
||||
```
|
||||
```text
|
||||
BREAKING CHANGE: <breaking change summary>
|
||||
<BLANK LINE>
|
||||
<breaking change description + migration instructions>
|
||||
|
@ -83,7 +87,7 @@ Fixes #<issue number>
|
|||
|
||||
or
|
||||
|
||||
```
|
||||
```text
|
||||
DEPRECATED: <what is deprecated>
|
||||
<BLANK LINE>
|
||||
<deprecation description + recommended update path>
|
||||
|
@ -95,18 +99,18 @@ Closes #<pr number>
|
|||
A Breaking Change section should start with the phrase "`BREAKING CHANGE:` " followed by a summary of the breaking change, a blank line, and a detailed description of the breaking change that also includes migration instructions.
|
||||
|
||||
> 💡 A breaking change can additionally also be marked by adding a “`!`” to the header, right before the “`:`”, e.g. `feat(editor)!: Remove support for dark mode`
|
||||
>
|
||||
>
|
||||
> This makes locating breaking changes easier when just skimming through commit messages.
|
||||
|
||||
> 💡 The breaking changes must also be added to the [packages/cli/BREAKING-CHANGES.md](https://github.com/n8n-io/n8n/blob/master/packages/cli/BREAKING-CHANGES.md) file located in the n8n repository.
|
||||
|
||||
Similarly, a Deprecation section should start with "`DEPRECATED:` " followed by a short description of what is deprecated, a blank line, and a detailed description of the deprecation that also mentions the recommended update path.
|
||||
|
||||
### **Revert commits**
|
||||
### Revert commits
|
||||
|
||||
If the commit reverts a previous commit, it should begin with `revert:` , followed by the header of the reverted commit.
|
||||
|
||||
The content of the commit message body should contain:
|
||||
|
||||
- information about the SHA of the commit being reverted in the following format: `This reverts commit <SHA>`,
|
||||
- a clear description of the reason for reverting the commit message.
|
||||
- a clear description of the reason for reverting the commit message.
|
||||
|
|
2
.github/scripts/package.json
vendored
2
.github/scripts/package.json
vendored
|
@ -5,7 +5,7 @@
|
|||
"debug": "4.3.4",
|
||||
"glob": "10.3.10",
|
||||
"p-limit": "3.1.0",
|
||||
"picocolors": "1.0.0",
|
||||
"picocolors": "1.0.1",
|
||||
"semver": "7.5.4",
|
||||
"tempfile": "5.0.0",
|
||||
"typescript": "*"
|
||||
|
|
6
.github/scripts/update-changelog.mjs
vendored
6
.github/scripts/update-changelog.mjs
vendored
|
@ -16,7 +16,11 @@ const changelogStream = conventionalChangelog({
|
|||
releaseCount: 1,
|
||||
tagPrefix: 'n8n@',
|
||||
transform: (commit, callback) => {
|
||||
callback(null, commit.header.includes('(no-changelog)') ? undefined : commit);
|
||||
const hasNoChangelogInHeader = commit.header.includes('(no-changelog)');
|
||||
const isBenchmarkScope = commit.scope === 'benchmark';
|
||||
|
||||
// Ignore commits that have 'benchmark' scope or '(no-changelog)' in the header
|
||||
callback(null, hasNoChangelogInHeader || isBenchmarkScope ? undefined : commit);
|
||||
},
|
||||
}).on('error', (err) => {
|
||||
console.error(err.stack);
|
||||
|
|
39
.github/workflows/benchmark-destroy-nightly.yml
vendored
Normal file
39
.github/workflows/benchmark-destroy-nightly.yml
vendored
Normal file
|
@ -0,0 +1,39 @@
|
|||
name: Destroy Benchmark Env
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 4 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
environment: benchmarking
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.1
|
||||
|
||||
- name: Azure login
|
||||
uses: azure/login@v2.1.1
|
||||
with:
|
||||
client-id: ${{ secrets.BENCHMARK_ARM_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.BENCHMARK_ARM_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.BENCHMARK_ARM_SUBSCRIPTION_ID }}
|
||||
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Destroy cloud env
|
||||
run: pnpm destroy-cloud-env
|
||||
working-directory: packages/@n8n/benchmark
|
97
.github/workflows/benchmark-nightly.yml
vendored
Normal file
97
.github/workflows/benchmark-nightly.yml
vendored
Normal file
|
@ -0,0 +1,97 @@
|
|||
name: Run Nightly Benchmark
|
||||
run-name: Benchmark ${{ inputs.n8n_tag || 'nightly' }}
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1,2,3 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
debug:
|
||||
description: 'Use debug logging'
|
||||
required: true
|
||||
default: 'false'
|
||||
n8n_tag:
|
||||
description: 'Name of the n8n docker tag to run the benchmark against.'
|
||||
required: true
|
||||
default: 'nightly'
|
||||
benchmark_tag:
|
||||
description: 'Name of the benchmark cli docker tag to run the benchmark with.'
|
||||
required: true
|
||||
default: 'latest'
|
||||
|
||||
env:
|
||||
ARM_CLIENT_ID: ${{ secrets.BENCHMARK_ARM_CLIENT_ID }}
|
||||
ARM_SUBSCRIPTION_ID: ${{ secrets.BENCHMARK_ARM_SUBSCRIPTION_ID }}
|
||||
ARM_TENANT_ID: ${{ secrets.BENCHMARK_ARM_TENANT_ID }}
|
||||
N8N_TAG: ${{ inputs.n8n_tag || 'nightly' }}
|
||||
N8N_BENCHMARK_TAG: ${{ inputs.benchmark_tag || 'latest' }}
|
||||
DEBUG: ${{ inputs.debug == 'true' && '--debug' || '' }}
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
environment: benchmarking
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.1
|
||||
|
||||
- uses: hashicorp/setup-terraform@v3
|
||||
with:
|
||||
terraform_version: '1.8.5'
|
||||
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Azure login
|
||||
uses: azure/login@v2.1.1
|
||||
with:
|
||||
client-id: ${{ env.ARM_CLIENT_ID }}
|
||||
tenant-id: ${{ env.ARM_TENANT_ID }}
|
||||
subscription-id: ${{ env.ARM_SUBSCRIPTION_ID }}
|
||||
|
||||
- name: Destroy any existing environment
|
||||
run: pnpm destroy-cloud-env
|
||||
working-directory: packages/@n8n/benchmark
|
||||
|
||||
- name: Provision the environment
|
||||
run: pnpm provision-cloud-env ${{ env.DEBUG }}
|
||||
working-directory: packages/@n8n/benchmark
|
||||
|
||||
- name: Run the benchmark
|
||||
env:
|
||||
BENCHMARK_RESULT_WEBHOOK_URL: ${{ secrets.BENCHMARK_RESULT_WEBHOOK_URL }}
|
||||
BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER: ${{ secrets.BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER }}
|
||||
N8N_LICENSE_CERT: ${{ secrets.N8N_BENCHMARK_LICENSE_CERT }}
|
||||
run: |
|
||||
pnpm benchmark-in-cloud \
|
||||
--vus 5 \
|
||||
--duration 1m \
|
||||
--n8nTag ${{ env.N8N_TAG }} \
|
||||
--benchmarkTag ${{ env.N8N_BENCHMARK_TAG }} \
|
||||
${{ env.DEBUG }}
|
||||
working-directory: packages/@n8n/benchmark
|
||||
|
||||
# We need to login again because the access token expires
|
||||
- name: Azure login
|
||||
if: always()
|
||||
uses: azure/login@v2.1.1
|
||||
with:
|
||||
client-id: ${{ env.ARM_CLIENT_ID }}
|
||||
tenant-id: ${{ env.ARM_TENANT_ID }}
|
||||
subscription-id: ${{ env.ARM_SUBSCRIPTION_ID }}
|
||||
|
||||
- name: Destroy the environment
|
||||
if: always()
|
||||
run: pnpm destroy-cloud-env ${{ env.DEBUG }}
|
||||
working-directory: packages/@n8n/benchmark
|
|
@ -26,7 +26,7 @@ jobs:
|
|||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build relevant packages
|
||||
run: pnpm --filter @n8n/client-oauth2 --filter @n8n/imap --filter n8n-workflow --filter n8n-core --filter n8n-nodes-base --filter @n8n/n8n-nodes-langchain build
|
||||
run: pnpm build:nodes
|
||||
|
||||
- run: npm install --prefix=.github/scripts --no-package-lock
|
||||
|
||||
|
|
5
.github/workflows/check-pr-title.yml
vendored
5
.github/workflows/check-pr-title.yml
vendored
|
@ -7,8 +7,7 @@ on:
|
|||
- edited
|
||||
- synchronize
|
||||
branches:
|
||||
- '**'
|
||||
- '!release/*'
|
||||
- 'master'
|
||||
|
||||
jobs:
|
||||
check-pr-title:
|
||||
|
@ -29,6 +28,6 @@ jobs:
|
|||
|
||||
- name: Validate PR title
|
||||
id: validate_pr_title
|
||||
uses: n8n-io/validate-n8n-pull-request-title@v2.0.1
|
||||
uses: n8n-io/validate-n8n-pull-request-title@v2.2.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
42
.github/workflows/chromatic.yml
vendored
42
.github/workflows/chromatic.yml
vendored
|
@ -4,19 +4,49 @@ on:
|
|||
workflow_dispatch:
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
branch:
|
||||
- 'master'
|
||||
paths:
|
||||
- packages/design-system/**
|
||||
- .github/workflows/chromatic.yml
|
||||
|
||||
concurrency:
|
||||
group: chromatic-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
get-metadata:
|
||||
name: Get Metadata
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out current commit
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Determine changed files
|
||||
uses: tomi/paths-filter-action@v3.0.2
|
||||
id: changed
|
||||
if: github.event_name == 'pull_request_review'
|
||||
with:
|
||||
filters: |
|
||||
design_system:
|
||||
- packages/design-system/**
|
||||
- .github/workflows/chromatic.yml
|
||||
|
||||
outputs:
|
||||
design_system_files_changed: ${{ steps.changed.outputs.design_system == 'true' }}
|
||||
is_community_pr: ${{ contains(github.event.pull_request.labels.*.name, 'community') }}
|
||||
is_pr_target_master: ${{ github.event.pull_request.base.ref == 'master' }}
|
||||
is_dispatch: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
is_pr_approved: ${{ github.event.review.state == 'approved' }}
|
||||
|
||||
chromatic:
|
||||
if: ${{ github.event.review.state == 'approved' && !contains(github.event.pull_request.labels.*.name, 'community') }}
|
||||
needs: [get-metadata]
|
||||
if: |
|
||||
needs.get-metadata.outputs.is_dispatch == 'true' ||
|
||||
(
|
||||
needs.get-metadata.outputs.design_system_files_changed == 'true' &&
|
||||
needs.get-metadata.outputs.is_community_pr == 'false' &&
|
||||
needs.get-metadata.outputs.is_pr_target_master == 'true' &&
|
||||
needs.get-metadata.outputs.is_pr_approved == 'true'
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
|
|
3
.github/workflows/ci-postgres-mysql.yml
vendored
3
.github/workflows/ci-postgres-mysql.yml
vendored
|
@ -8,6 +8,8 @@ on:
|
|||
paths:
|
||||
- packages/cli/src/databases/**
|
||||
- .github/workflows/ci-postgres-mysql.yml
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
concurrency:
|
||||
group: db-${{ github.event.pull_request.number || github.ref }}
|
||||
|
@ -17,6 +19,7 @@ jobs:
|
|||
build:
|
||||
name: Install & Build
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request_review' || startsWith(github.event.pull_request.base.ref, 'release/')
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
- run: corepack enable
|
||||
|
|
7
.github/workflows/ci-pull-requests.yml
vendored
7
.github/workflows/ci-pull-requests.yml
vendored
|
@ -1,6 +1,10 @@
|
|||
name: Build, unit test and lint branch
|
||||
|
||||
on: [pull_request]
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
- '!release/*'
|
||||
|
||||
jobs:
|
||||
install-and-build:
|
||||
|
@ -9,7 +13,6 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
repository: n8n-io/n8n
|
||||
ref: refs/pull/${{ github.event.pull_request.number }}/merge
|
||||
|
||||
- run: corepack enable
|
||||
|
|
43
.github/workflows/docker-images-benchmark.yml
vendored
Normal file
43
.github/workflows/docker-images-benchmark.yml
vendored
Normal file
|
@ -0,0 +1,43 @@
|
|||
name: Benchmark Docker Image CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'packages/@n8n/benchmark/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'pnpm-workspace.yaml'
|
||||
- '.github/workflows/docker-images-benchmark.yml'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: .
|
||||
file: ./packages/@n8n/benchmark/Dockerfile
|
||||
platforms: linux/amd64
|
||||
provenance: false
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/${{ github.repository_owner }}/n8n-benchmark:latest
|
27
.github/workflows/docker-images-nightly.yml
vendored
27
.github/workflows/docker-images-nightly.yml
vendored
|
@ -6,10 +6,6 @@ on:
|
|||
- cron: '0 1 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
repository:
|
||||
description: 'GitHub repository to create image off.'
|
||||
required: true
|
||||
default: 'n8n-io/n8n'
|
||||
branch:
|
||||
description: 'GitHub branch to create image off.'
|
||||
required: true
|
||||
|
@ -36,6 +32,9 @@ on:
|
|||
required: false
|
||||
default: ''
|
||||
|
||||
env:
|
||||
N8N_TAG: ${{ inputs.tag || 'nightly' }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -49,7 +48,6 @@ jobs:
|
|||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
repository: ${{ github.event.inputs.repository || 'n8n-io/n8n' }}
|
||||
ref: ${{ github.event.inputs.branch || 'master' }}
|
||||
|
||||
- name: Set up QEMU
|
||||
|
@ -69,7 +67,7 @@ jobs:
|
|||
[[ "${{github.event.inputs.merge-master}}" == "true" ]] && git remote add upstream https://github.com/n8n-io/n8n.git -f; git merge upstream/master --allow-unrelated-histories || echo ""
|
||||
shell: bash
|
||||
|
||||
- name: Build and push
|
||||
- name: Build and push to DockerHub
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: .
|
||||
|
@ -81,7 +79,22 @@ jobs:
|
|||
push: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
tags: ${{ secrets.DOCKER_USERNAME }}/n8n:${{ github.event.inputs.tag || 'nightly' }}
|
||||
tags: ${{ secrets.DOCKER_USERNAME }}/n8n:${{ env.N8N_TAG }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: env.N8N_TAG == 'nightly'
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Push image to GHCR
|
||||
if: env.N8N_TAG == 'nightly'
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
--tag ghcr.io/${{ github.repository_owner }}/n8n:nightly \
|
||||
${{ secrets.DOCKER_USERNAME }}/n8n:nightly
|
||||
|
||||
- name: Call Success URL - optionally
|
||||
run: |
|
||||
|
|
48
.github/workflows/docker-images.yml
vendored
48
.github/workflows/docker-images.yml
vendored
|
@ -1,48 +0,0 @@
|
|||
name: Docker Image CI
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
|
||||
- name: Get the version
|
||||
id: vars
|
||||
run: echo ::set-output name=tag::$(echo ${GITHUB_REF:14})
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: ./docker/images/n8n
|
||||
build-args: |
|
||||
N8N_VERSION=${{ steps.vars.outputs.tag }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
provenance: false
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKER_USERNAME }}/n8n:${{ steps.vars.outputs.tag }}
|
||||
ghcr.io/${{ github.repository_owner }}/n8n:${{ steps.vars.outputs.tag }}
|
11
.github/workflows/e2e-reusable.yml
vendored
11
.github/workflows/e2e-reusable.yml
vendored
|
@ -22,11 +22,6 @@ on:
|
|||
required: false
|
||||
default: 'browsers:node18.12.0-chrome107'
|
||||
type: string
|
||||
cache-key:
|
||||
description: 'Cache key for modules and build artifacts.'
|
||||
required: false
|
||||
default: ${{ github.sha }}-${{ inputs.run-env }}-e2e-modules
|
||||
type: string
|
||||
record:
|
||||
description: 'Record test run.'
|
||||
required: false
|
||||
|
@ -78,7 +73,6 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
repository: n8n-io/n8n
|
||||
ref: ${{ inputs.branch }}
|
||||
|
||||
- name: Checkout PR
|
||||
|
@ -111,7 +105,7 @@ jobs:
|
|||
/github/home/.cache
|
||||
/github/home/.pnpm-store
|
||||
./packages/**/dist
|
||||
key: ${{ inputs.cache-key }}
|
||||
key: ${{ github.sha }}-e2e
|
||||
|
||||
testing:
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -128,7 +122,6 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
repository: n8n-io/n8n
|
||||
ref: ${{ inputs.branch }}
|
||||
|
||||
- name: Checkout PR
|
||||
|
@ -146,7 +139,7 @@ jobs:
|
|||
/github/home/.cache
|
||||
/github/home/.pnpm-store
|
||||
./packages/**/dist
|
||||
key: ${{ inputs.cache-key }}
|
||||
key: ${{ github.sha }}-e2e
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
|
44
.github/workflows/e2e-tests-pr.yml
vendored
44
.github/workflows/e2e-tests-pr.yml
vendored
|
@ -3,33 +3,65 @@ name: PR E2E
|
|||
on:
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
branch:
|
||||
- 'master'
|
||||
|
||||
concurrency:
|
||||
group: e2e-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
get-metadata:
|
||||
name: Get Metadata
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out current commit
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Determine changed files
|
||||
uses: tomi/paths-filter-action@v3.0.2
|
||||
id: changed
|
||||
with:
|
||||
filters: |
|
||||
not_ignored:
|
||||
- '!.devcontainer/**'
|
||||
- '!.github/*'
|
||||
- '!.github/scripts/*'
|
||||
- '!.github/workflows/benchmark-*'
|
||||
- '!.github/workflows/check-*'
|
||||
- '!.vscode/**'
|
||||
- '!docker/**'
|
||||
- '!packages/@n8n/benchmark/**'
|
||||
- '!**/*.md'
|
||||
predicate-quantifier: 'every'
|
||||
|
||||
outputs:
|
||||
# The workflow should run when:
|
||||
# - It has changes to files that are not ignored
|
||||
# - It is not a community PR
|
||||
# - It is targeting master or a release branch
|
||||
should_run: ${{ steps.changed.outputs.not_ignored == 'true' && !contains(github.event.pull_request.labels.*.name, 'community') && (github.event.pull_request.base.ref == 'master' || startsWith(github.event.pull_request.base.ref, 'release/')) }}
|
||||
|
||||
run-e2e-tests:
|
||||
name: E2E [Electron/Node 18]
|
||||
uses: ./.github/workflows/e2e-reusable.yml
|
||||
if: ${{ github.event.review.state == 'approved' && !contains(github.event.pull_request.labels.*.name, 'community') }}
|
||||
needs: [get-metadata]
|
||||
if: ${{ github.event.review.state == 'approved' && needs.get-metadata.outputs.should_run == 'true' }}
|
||||
with:
|
||||
pr_number: ${{ github.event.pull_request.number }}
|
||||
user: ${{ github.event.pull_request.user.login || 'PR User' }}
|
||||
spec: 'e2e/*'
|
||||
secrets:
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
|
||||
post-e2e-tests:
|
||||
runs-on: ubuntu-latest
|
||||
name: E2E [Electron/Node 18] - Checks
|
||||
needs: [run-e2e-tests]
|
||||
needs: [get-metadata, run-e2e-tests]
|
||||
if: always()
|
||||
steps:
|
||||
- name: E2E success comment
|
||||
if: ${{!contains(github.event.pull_request.labels.*.name, 'community') && needs.run-e2e-tests.outputs.tests_passed == 'true' }}
|
||||
if: ${{ needs.get-metadata.outputs.should_run == 'true' && needs.run-e2e-tests.outputs.tests_passed == 'true' }}
|
||||
uses: peter-evans/create-or-update-comment@v4.0.0
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
|
|
1
.github/workflows/linting-reusable.yml
vendored
1
.github/workflows/linting-reusable.yml
vendored
|
@ -21,7 +21,6 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
repository: n8n-io/n8n
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- run: corepack enable
|
||||
|
|
6
.github/workflows/release-create-pr.yml
vendored
6
.github/workflows/release-create-pr.yml
vendored
|
@ -56,12 +56,12 @@ jobs:
|
|||
git push -f origin refs/remotes/origin/${{ github.event.inputs.base-branch }}:refs/heads/release/${{ env.NEXT_RELEASE }}
|
||||
|
||||
- name: Push the release branch, and Create the PR
|
||||
uses: peter-evans/create-pull-request@v5
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
base: 'release/${{ env.NEXT_RELEASE }}'
|
||||
branch: '${{ env.NEXT_RELEASE }}-pr'
|
||||
branch: 'release-pr/${{ env.NEXT_RELEASE }}'
|
||||
commit-message: ':rocket: Release ${{ env.NEXT_RELEASE }}'
|
||||
delete-branch: true
|
||||
labels: 'release'
|
||||
labels: release,release:${{ github.event.inputs.release-type }}
|
||||
title: ':rocket: Release ${{ env.NEXT_RELEASE }}'
|
||||
body-path: 'CHANGELOG-${{ env.NEXT_RELEASE }}.md'
|
||||
|
|
109
.github/workflows/release-publish.yml
vendored
109
.github/workflows/release-publish.yml
vendored
|
@ -8,18 +8,17 @@ on:
|
|||
- 'release/*'
|
||||
|
||||
jobs:
|
||||
publish-release:
|
||||
if: github.event.pull_request.merged == true
|
||||
publish-to-npm:
|
||||
name: Publish to NPM
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
if: github.event.pull_request.merged == true
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
|
||||
outputs:
|
||||
release: ${{ steps.set-release.outputs.release }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.1
|
||||
|
@ -51,25 +50,97 @@ jobs:
|
|||
pnpm publish -r --publish-branch ${{github.event.pull_request.base.ref}} --access public --tag rc --no-git-checks
|
||||
npm dist-tag rm n8n rc
|
||||
|
||||
- id: set-release
|
||||
run: echo "release=${{ env.RELEASE }}" >> $GITHUB_OUTPUT
|
||||
|
||||
publish-to-docker-hub:
|
||||
name: Publish to DockerHub
|
||||
needs: [publish-to-npm]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.merged == true
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: ./docker/images/n8n
|
||||
build-args: |
|
||||
N8N_VERSION=${{ needs.publish-to-npm.outputs.release }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
provenance: false
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKER_USERNAME }}/n8n:${{ needs.publish-to-npm.outputs.release }}
|
||||
ghcr.io/${{ github.repository_owner }}/n8n:${{ needs.publish-to-npm.outputs.release }}
|
||||
|
||||
create-github-release:
|
||||
name: Create a GitHub Release
|
||||
needs: [publish-to-npm, publish-to-docker-hub]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.merged == true
|
||||
timeout-minutes: 5
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Create a Release on GitHub
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
commit: ${{github.event.pull_request.base.ref}}
|
||||
tag: 'n8n@${{env.RELEASE}}'
|
||||
tag: 'n8n@${{ needs.publish-to-npm.outputs.release }}'
|
||||
prerelease: true
|
||||
makeLatest: false
|
||||
body: ${{github.event.pull_request.body}}
|
||||
|
||||
trigger-release-note:
|
||||
name: Trigger a release note
|
||||
needs: [publish-to-npm, create-github-release]
|
||||
if: github.event.pull_request.merged == true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger a release note
|
||||
continue-on-error: true
|
||||
run: curl -u docsWorkflows:${{ secrets.N8N_WEBHOOK_DOCS_PASSWORD }} --request GET 'https://internal.users.n8n.cloud/webhook/trigger-release-note' --header 'Content-Type:application/json' --data '{"version":"${{env.RELEASE}}"}'
|
||||
run: curl -u docsWorkflows:${{ secrets.N8N_WEBHOOK_DOCS_PASSWORD }} --request GET 'https://internal.users.n8n.cloud/webhook/trigger-release-note' --header 'Content-Type:application/json' --data '{"version":"${{ needs.publish-to-npm.outputs.release }}"}'
|
||||
|
||||
# - name: Merge Release into 'master'
|
||||
# run: |
|
||||
# git fetch origin
|
||||
# git checkout --track origin/master
|
||||
# git config user.name "Jan Oberhauser"
|
||||
# git config user.email jan.oberhauser@gmail.com
|
||||
# git merge --ff n8n@${{env.RELEASE}}
|
||||
# git push origin master
|
||||
# git push origin :${{github.event.pull_request.base.ref}}
|
||||
merge-back-into-master:
|
||||
name: Merge back into master
|
||||
needs: [publish-to-npm, create-github-release]
|
||||
if: ${{ github.event.pull_request.merged == true && !contains(github.event.pull_request.labels.*.name, 'release:patch') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: |
|
||||
git checkout --track origin/master
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
|
||||
git merge --ff n8n@${{ needs.publish-to-npm.outputs.release }}
|
||||
git push origin master
|
||||
git push origin :${{github.event.pull_request.base.ref}}
|
||||
|
|
1
.github/workflows/test-workflows.yml
vendored
1
.github/workflows/test-workflows.yml
vendored
|
@ -73,6 +73,7 @@ jobs:
|
|||
env:
|
||||
N8N_ENCRYPTION_KEY: ${{secrets.ENCRYPTION_KEY}}
|
||||
SKIP_STATISTICS_EVENTS: true
|
||||
DB_SQLITE_POOL_SIZE: 4
|
||||
# -
|
||||
# name: Export credentials
|
||||
# if: always()
|
||||
|
|
2
.github/workflows/units-tests-reusable.yml
vendored
2
.github/workflows/units-tests-reusable.yml
vendored
|
@ -36,7 +36,6 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
repository: n8n-io/n8n
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- run: corepack enable
|
||||
|
@ -50,6 +49,7 @@ jobs:
|
|||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup build cache
|
||||
if: inputs.collectCoverage != true
|
||||
uses: rharkor/caching-for-turbo@v1.5
|
||||
|
||||
- name: Build
|
||||
|
|
1
.vscode/extensions.json
vendored
1
.vscode/extensions.json
vendored
|
@ -5,6 +5,7 @@
|
|||
"dbaeumer.vscode-eslint",
|
||||
"EditorConfig.EditorConfig",
|
||||
"esbenp.prettier-vscode",
|
||||
"mjmlio.vscode-mjml",
|
||||
"Vue.volar"
|
||||
]
|
||||
}
|
||||
|
|
287
CHANGELOG.md
287
CHANGELOG.md
|
@ -1,3 +1,290 @@
|
|||
# [1.59.0](https://github.com/n8n-io/n8n/compare/n8n@1.58.0...n8n@1.59.0) (2024-09-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Chat Trigger Node:** Fix auth in "Embedded Chat" mode ([#10734](https://github.com/n8n-io/n8n/issues/10734)) ([96db501](https://github.com/n8n-io/n8n/commit/96db501a615ff7ec91bb66ea49532a2c6ca2a172))
|
||||
* **core:** Allow license:clear command to be used for licenses that failed renewal ([#10665](https://github.com/n8n-io/n8n/issues/10665)) ([a422c5a](https://github.com/n8n-io/n8n/commit/a422c5ac7b8f609eeab891230d9660f71bf225c5))
|
||||
* **core:** Update subworkflow execution status correctly ([#10764](https://github.com/n8n-io/n8n/issues/10764)) ([4f94319](https://github.com/n8n-io/n8n/commit/4f94319cd93885ebe830fa1f0e6b757de80f7356))
|
||||
* **editor:** Add arrow end to connection line ([#10704](https://github.com/n8n-io/n8n/issues/10704)) ([43713dc](https://github.com/n8n-io/n8n/commit/43713dcd89fcb98ea7e24d27127861fc4b0d7872))
|
||||
* **editor:** Add sticky note readonly state in new canvas ([#10678](https://github.com/n8n-io/n8n/issues/10678)) ([c5bc8e6](https://github.com/n8n-io/n8n/commit/c5bc8e6eb9eadadf44f763e5e5aac4b35d03cc31))
|
||||
* **editor:** Auto-focus expression input when switching from "fixed" mode ([#10686](https://github.com/n8n-io/n8n/issues/10686)) ([54ab2b1](https://github.com/n8n-io/n8n/commit/54ab2b14e41fe84a455c7e7d5c73d7347844d2fb))
|
||||
* **editor:** Don't render pinned icon for disabled nodes ([#10712](https://github.com/n8n-io/n8n/issues/10712)) ([879b837](https://github.com/n8n-io/n8n/commit/879b8375812106b3f6909b7de27858175ba5575d))
|
||||
* **editor:** Fix error rendering and indexing of LLM sub-node outputs ([#10688](https://github.com/n8n-io/n8n/issues/10688)) ([50459ba](https://github.com/n8n-io/n8n/commit/50459bacab517bacb97d2884fda69f8412c9960c))
|
||||
* **editor:** Fix xss issues in toast usages ([#10733](https://github.com/n8n-io/n8n/issues/10733)) ([6df6f5f](https://github.com/n8n-io/n8n/commit/6df6f5f8df9a8fc0899524a1b69859815eeb341f))
|
||||
* **editor:** Follow up fixes and improvements to viewer role ([#10684](https://github.com/n8n-io/n8n/issues/10684)) ([63548e6](https://github.com/n8n-io/n8n/commit/63548e6ead5c122732628b5feb1515f492d5e033))
|
||||
* **editor:** Increase connector snap radius ([#10757](https://github.com/n8n-io/n8n/issues/10757)) ([297b668](https://github.com/n8n-io/n8n/commit/297b668f32f9ecfc82c1205ea4e915408cab482e))
|
||||
* **editor:** Plus node button should not be visible on readonly mode ([#10692](https://github.com/n8n-io/n8n/issues/10692)) ([62cb189](https://github.com/n8n-io/n8n/commit/62cb189985035c447ad31c275337b3fb24089265))
|
||||
* **editor:** Prevent action's panel flickering while dragging a node ([#10739](https://github.com/n8n-io/n8n/issues/10739)) ([efa5573](https://github.com/n8n-io/n8n/commit/efa5573278a60d55d5b509aac48cc112c79334d2))
|
||||
* **editor:** Restrict when the collision avoidance algorithm is used ([#10755](https://github.com/n8n-io/n8n/issues/10755)) ([bf43d67](https://github.com/n8n-io/n8n/commit/bf43d673571b2fc18fe5d660171f0da165909dfc))
|
||||
* **editor:** Show docs link in credential modal when docs sidebar is hidden ([#10750](https://github.com/n8n-io/n8n/issues/10750)) ([87333cb](https://github.com/n8n-io/n8n/commit/87333cbefebe652256fa1d60ba7a4b946fdfe17d))
|
||||
* **Email Trigger (IMAP) Node:** Ensure connection close does not block deactivation ([#10689](https://github.com/n8n-io/n8n/issues/10689)) ([156eb72](https://github.com/n8n-io/n8n/commit/156eb72ebefa1d963ff46eff6652e2c947ef031b))
|
||||
* Fix the issue in Trigger Nodes where poll time was not loaded ([#10695](https://github.com/n8n-io/n8n/issues/10695)) ([1dea8f4](https://github.com/n8n-io/n8n/commit/1dea8f4c7da2a04434c274faf8e0a9a7a693f5a4))
|
||||
* **Gmail Trigger Node:** Change Gmail Trigger dedupe logic ([#10717](https://github.com/n8n-io/n8n/issues/10717)) ([9f3e03d](https://github.com/n8n-io/n8n/commit/9f3e03d728d8acda5ae4166c5837b00cb1311e96))
|
||||
* Google Contacts node warm up request, Google Calendar node events>getAll fields option ([#10700](https://github.com/n8n-io/n8n/issues/10700)) ([22c70d5](https://github.com/n8n-io/n8n/commit/22c70d50697023cf448a379d7778695abb718ce9))
|
||||
* **If Node:** Update copy for type conversion parameter ([#10769](https://github.com/n8n-io/n8n/issues/10769)) ([ee5fbc5](https://github.com/n8n-io/n8n/commit/ee5fbc543ce1d33a56cf118dbd048d6693a15875))
|
||||
* **n8n Form Trigger Node:** Do not rerun trigger when it has run data ([#10687](https://github.com/n8n-io/n8n/issues/10687)) ([3adbcab](https://github.com/n8n-io/n8n/commit/3adbcab27de34ea5a2c7a88b2ad0d80e3f6d4a0b))
|
||||
* **OpenAI Chat Model Node:** Prevent filtering of fine-tuned models in model selector ([#10662](https://github.com/n8n-io/n8n/issues/10662)) ([4e89912](https://github.com/n8n-io/n8n/commit/4e899125884bdd97c97446d90e89668688fe7573))
|
||||
* Prevent AI assistant session reset when workflow is saved ([#10707](https://github.com/n8n-io/n8n/issues/10707)) ([91d9be2](https://github.com/n8n-io/n8n/commit/91d9be20667c20599f64a24fa99386c78476d425))
|
||||
* Show a more user friendly error message if initial Db connection times out ([#10682](https://github.com/n8n-io/n8n/issues/10682)) ([4efcbc5](https://github.com/n8n-io/n8n/commit/4efcbc593685286837022e5600d81e67f3e0131c))
|
||||
* **Webflow Node:** Update scopes to include forms ([#10554](https://github.com/n8n-io/n8n/issues/10554)) ([d3861b3](https://github.com/n8n-io/n8n/commit/d3861b31ceef16f566c525c7651453a1b84ed2a4))
|
||||
* **YouTube Node:** Fix Date filters ([#10725](https://github.com/n8n-io/n8n/issues/10725)) ([21936c8](https://github.com/n8n-io/n8n/commit/21936c88a84b8c03a8d02391cb7112b0e4d9f1f9))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Code Tool Node:** Option to specify input schema ([#10693](https://github.com/n8n-io/n8n/issues/10693)) ([421aa71](https://github.com/n8n-io/n8n/commit/421aa712515d9beeae7c0201b173cb7324473f69))
|
||||
* **editor:** Add lint for $('Node').item in runOnceForAllItems mode ([#10743](https://github.com/n8n-io/n8n/issues/10743)) ([1b04be1](https://github.com/n8n-io/n8n/commit/1b04be1240ec29151e79162680907710c71c6488))
|
||||
* **editor:** Logs markdown block improvements ([#10681](https://github.com/n8n-io/n8n/issues/10681)) ([db6e832](https://github.com/n8n-io/n8n/commit/db6e8326c7119d90fa6a51f82099026f50587202))
|
||||
* Filter parameter: Improve loose type validation for booleans ([#10702](https://github.com/n8n-io/n8n/issues/10702)) ([e9b8d99](https://github.com/n8n-io/n8n/commit/e9b8d99084f0ea2063a1d691928025e534980b4e))
|
||||
* **Lemlist Node:** Add V2 to support more API operations ([#10615](https://github.com/n8n-io/n8n/issues/10615)) ([20b1cf2](https://github.com/n8n-io/n8n/commit/20b1cf2b7597c78e28f522945b8cbad2ee535cd7))
|
||||
* **OpenAI Node:** Add Max Tools Iteration parameter and prevent tool calling after execution is aborted ([#10735](https://github.com/n8n-io/n8n/issues/10735)) ([5c47a5f](https://github.com/n8n-io/n8n/commit/5c47a5f691d42dae84a9df8a32a5ea600d83f6dd))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **editor:** Fix WorkflowDetails excessive re-rendering ([#10767](https://github.com/n8n-io/n8n/issues/10767)) ([00013a2](https://github.com/n8n-io/n8n/commit/00013a2069fff5e5d9398c5921c90d34dc384299))
|
||||
|
||||
|
||||
|
||||
# [1.58.0](https://github.com/n8n-io/n8n/compare/n8n@1.57.0...n8n@1.58.0) (2024-09-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **AI Agent Node:** Fix tools agent when using memory and Anthropic models ([#10513](https://github.com/n8n-io/n8n/issues/10513)) ([746e7b8](https://github.com/n8n-io/n8n/commit/746e7b89f7e9b99126fb69110773548dfe91b74f))
|
||||
* **API:** Update express-openapi-validator to resolve AIKIDO-2024-10229 ([#10612](https://github.com/n8n-io/n8n/issues/10612)) ([1dcb814](https://github.com/n8n-io/n8n/commit/1dcb814ced7cfbc80eddbb4bc03108341a9f27f5))
|
||||
* **core:** Declutter webhook insertion errors ([#10650](https://github.com/n8n-io/n8n/issues/10650)) ([36177b0](https://github.com/n8n-io/n8n/commit/36177b0943cf72bae3b0075453498dd1e41684d0))
|
||||
* **core:** Flush responses for ai streaming endpoints ([#10633](https://github.com/n8n-io/n8n/issues/10633)) ([6bb6a5c](https://github.com/n8n-io/n8n/commit/6bb6a5c6cd1da3503a1a2b35bcf4c685cd3f964f))
|
||||
* **core:** Tighten check for company size survey answer ([#10646](https://github.com/n8n-io/n8n/issues/10646)) ([e5aba60](https://github.com/n8n-io/n8n/commit/e5aba60afff93364d91f17c00ea18d38d9dbc970))
|
||||
* **editor:** Add confirmation toast when changing user role ([#10592](https://github.com/n8n-io/n8n/issues/10592)) ([95da4d4](https://github.com/n8n-io/n8n/commit/95da4d4797e800c04b2b17c23c941c785dd62393))
|
||||
* **editor:** Add pinned data only to manual executions in execution view ([#10605](https://github.com/n8n-io/n8n/issues/10605)) ([a12e9ed](https://github.com/n8n-io/n8n/commit/a12e9edac042957939c63f0a5c35572930632352))
|
||||
* **editor:** Add tooltips to workflow history button ([#10570](https://github.com/n8n-io/n8n/issues/10570)) ([4a125f5](https://github.com/n8n-io/n8n/commit/4a125f511c5537977652900b7712a2ad908140e7))
|
||||
* **editor:** Allow disabling SSO when config request fails ([#10635](https://github.com/n8n-io/n8n/issues/10635)) ([ce39933](https://github.com/n8n-io/n8n/commit/ce39933766fa18107f4082de0cba0b6702cbbbfa))
|
||||
* **editor:** Fix notification rendering HTML as text ([#10642](https://github.com/n8n-io/n8n/issues/10642)) ([5eba534](https://github.com/n8n-io/n8n/commit/5eba5343191665cd4639632ba303464176c279c4))
|
||||
* **editor:** Fix opening executions tab from a new, unsaved workflow ([#10652](https://github.com/n8n-io/n8n/issues/10652)) ([cd0891e](https://github.com/n8n-io/n8n/commit/cd0891e4f1cfdc90b2090958a39564ba99534627))
|
||||
* **Gmail Trigger Node:** Don't return date instances, but date strings instead ([#10582](https://github.com/n8n-io/n8n/issues/10582)) ([9e1dac0](https://github.com/n8n-io/n8n/commit/9e1dac04655a20c5c7b99552742312fd9237604b))
|
||||
* **HTTP Request Node:** Sanitize authorization headers ([#10607](https://github.com/n8n-io/n8n/issues/10607)) ([405c55a](https://github.com/n8n-io/n8n/commit/405c55a1f7cf34e7b6e46a86031ef9a41956ca78))
|
||||
* **Wait Node:** Append n8n attribution option ([#10585](https://github.com/n8n-io/n8n/issues/10585)) ([81f4322](https://github.com/n8n-io/n8n/commit/81f4322d456773281aec4b47447465bdffd311fe))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **core:** Execution curation ([#10342](https://github.com/n8n-io/n8n/issues/10342)) ([022ddcb](https://github.com/n8n-io/n8n/commit/022ddcbef9f1ac1b89bcfd5f7759d67325b07392))
|
||||
* **core:** Implement wrapping of regular nodes as AI Tools ([#10641](https://github.com/n8n-io/n8n/issues/10641)) ([da44fe4](https://github.com/n8n-io/n8n/commit/da44fe4b8967055b7b1f849750e1fafa0ba67218))
|
||||
* **core:** Introduce DB health check ([#10661](https://github.com/n8n-io/n8n/issues/10661)) ([a8e80d0](https://github.com/n8n-io/n8n/commit/a8e80d0c4b7531fe32be1d4057656885359f42fc))
|
||||
* **core:** Make Postgres connection timeout configurable ([#10670](https://github.com/n8n-io/n8n/issues/10670)) ([8154031](https://github.com/n8n-io/n8n/commit/81540318b4c55f3a09c9776e23d2211abdbd36f7))
|
||||
* **core:** Switch to MJML for email templates ([#10518](https://github.com/n8n-io/n8n/issues/10518)) ([dbc10fe](https://github.com/n8n-io/n8n/commit/dbc10fe9f522f31eb06add6f3f6863ce24510547))
|
||||
* **editor:** Add A/B testing feature flag for credential docs modal ([#10664](https://github.com/n8n-io/n8n/issues/10664)) ([899b0a1](https://github.com/n8n-io/n8n/commit/899b0a19efc49c1c087f78bbb1a59d726a510965))
|
||||
* **editor:** Add AI Assistant support chat ([#10656](https://github.com/n8n-io/n8n/issues/10656)) ([3a80780](https://github.com/n8n-io/n8n/commit/3a8078068e5c0b01dfd34ff838fe1b30d604abc6))
|
||||
* **editor:** Implement new app layout ([#10548](https://github.com/n8n-io/n8n/issues/10548)) ([95a9cd2](https://github.com/n8n-io/n8n/commit/95a9cd2c739cf4f817eb8df6509a9112ac24a3b1))
|
||||
* **editor:** Make highlighted data pane floating ([#10638](https://github.com/n8n-io/n8n/issues/10638)) ([8b5c333](https://github.com/n8n-io/n8n/commit/8b5c333d3dca03ba51a5873b75451fbfafc5ae15))
|
||||
* More hints to nodes ([#10565](https://github.com/n8n-io/n8n/issues/10565)) ([66ddb4a](https://github.com/n8n-io/n8n/commit/66ddb4a6f367602c9aaad1bfb0cc6fac3facd15e))
|
||||
* **Postgres PGVector Store Node:** Add PGVector vector store node ([#10517](https://github.com/n8n-io/n8n/issues/10517)) ([650389d](https://github.com/n8n-io/n8n/commit/650389d90763a45c037e74a1a1193c3cbe103a16))
|
||||
* Reintroduce collaboration feature ([#10602](https://github.com/n8n-io/n8n/issues/10602)) ([2ea2bfe](https://github.com/n8n-io/n8n/commit/2ea2bfe762c02047e522f28dd97f197735b3fb46))
|
||||
* **Text Classifier Node:** Add output fixing parser ([#10667](https://github.com/n8n-io/n8n/issues/10667)) ([aa37c32](https://github.com/n8n-io/n8n/commit/aa37c32f266ffff93cd903888b1c15caa0468830))
|
||||
|
||||
|
||||
|
||||
# [1.57.0](https://github.com/n8n-io/n8n/compare/n8n@1.56.0...n8n@1.57.0) (2024-08-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **AI Agent Node:** Allow AWS Bedrock Chat to be used with conversational agent ([#10489](https://github.com/n8n-io/n8n/issues/10489)) ([bdcc657](https://github.com/n8n-io/n8n/commit/bdcc657965af5f604aac1eaff7d937f69a08ce1c))
|
||||
* **core:** Make boolean config value parsing backward-compatible ([#10560](https://github.com/n8n-io/n8n/issues/10560)) ([70b410f](https://github.com/n8n-io/n8n/commit/70b410f4b00dd599fcd4249aa105098aa262da66))
|
||||
* **core:** Restore Redis cache key ([#10520](https://github.com/n8n-io/n8n/issues/10520)) ([873056a](https://github.com/n8n-io/n8n/commit/873056a92e52cc629d2873c960656d5f06d4728e))
|
||||
* **core:** Scheduler tasks should not trigger on follower instances ([#10507](https://github.com/n8n-io/n8n/issues/10507)) ([3428f28](https://github.com/n8n-io/n8n/commit/3428f28a732f79e067b3cb515cc59d835de246a6))
|
||||
* **core:** Stop explicit redis client disconnect on shutdown ([#10551](https://github.com/n8n-io/n8n/issues/10551)) ([f712812](https://github.com/n8n-io/n8n/commit/f71281221efb79d65d8d7610c292bc90cef13d7a))
|
||||
* **editor:** Ensure `Datatable` component renders `All` option ([#10525](https://github.com/n8n-io/n8n/issues/10525)) ([bc27beb](https://github.com/n8n-io/n8n/commit/bc27beb6629883003a8991d7e840ffaa066d41ac))
|
||||
* **editor:** Prevent Safari users from accessing the frontend over insecure contexts ([#10510](https://github.com/n8n-io/n8n/issues/10510)) ([a73b9a3](https://github.com/n8n-io/n8n/commit/a73b9a38d6c48e2f78593328e7d9933f2493dbb6))
|
||||
* **editor:** Scale output item selector input width with value ([#10555](https://github.com/n8n-io/n8n/issues/10555)) ([52c574d](https://github.com/n8n-io/n8n/commit/52c574d83f344f03b0e39984bbc3ac0402e50791))
|
||||
* **Google Sheets Trigger Node:** Show sheet name is too long error ([#10542](https://github.com/n8n-io/n8n/issues/10542)) ([4e15007](https://github.com/n8n-io/n8n/commit/4e1500757700ec984cdad8b9cfcd76ee00ae127e))
|
||||
* **Wait Node:** Prevent waiting until invalid date ([#10523](https://github.com/n8n-io/n8n/issues/10523)) ([c0e7620](https://github.com/n8n-io/n8n/commit/c0e7620036738f8d0b382d0d0610b981dcbc29e0))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add new credentials for the HTTP Request node ([#9833](https://github.com/n8n-io/n8n/issues/9833)) ([26f1af3](https://github.com/n8n-io/n8n/commit/26f1af397b2b25e3394fc2dae91a5c281bf33d66))
|
||||
* **AI Agent Node:** Add tutorial link to agent node ([#10493](https://github.com/n8n-io/n8n/issues/10493)) ([5c7cc36](https://github.com/n8n-io/n8n/commit/5c7cc36c23e58a47a1e71911e7303a1bd54f167e))
|
||||
* **core:** Expose queue metrics for Prometheus ([#10559](https://github.com/n8n-io/n8n/issues/10559)) ([008c510](https://github.com/n8n-io/n8n/commit/008c510b7623fefb8c60730c7eac54dd9bb2e3fc))
|
||||
* **editor:** Implement workflowSelector parameter type ([#10482](https://github.com/n8n-io/n8n/issues/10482)) ([84e54be](https://github.com/n8n-io/n8n/commit/84e54beac763f25399c9687f695f1e658e3ce434))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **core:** Make execution queries faster ([#9817](https://github.com/n8n-io/n8n/issues/9817)) ([dc7dc99](https://github.com/n8n-io/n8n/commit/dc7dc995d5e2ea8fbd0dcb54cfa8aa93ecb437c9))
|
||||
|
||||
### Other
|
||||
* **Add user journey link to [n8n.io](https://n8n.io)** ([#10331](https://github.com/n8n-io/n8n/pull/10331))
|
||||
|
||||
|
||||
# [1.56.0](https://github.com/n8n-io/n8n/compare/n8n@1.55.0...n8n@1.56.0) (2024-08-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Better errors in Switch, If and Filter nodes ([#10457](https://github.com/n8n-io/n8n/issues/10457)) ([aea82cb](https://github.com/n8n-io/n8n/commit/aea82cb74421d516919742127daf669808b57604))
|
||||
* **Calendly Trigger Node:** Fix issue with webhook url matching ([#10378](https://github.com/n8n-io/n8n/issues/10378)) ([09c3a8b](https://github.com/n8n-io/n8n/commit/09c3a8b36733a9634ef5948922d6aa7a19bbb592))
|
||||
* **core:** Fix payload property in `workflow-post-execute` event ([#10413](https://github.com/n8n-io/n8n/issues/10413)) ([d98e29e](https://github.com/n8n-io/n8n/commit/d98e29e3d53de87aec276260615fa60473a2692f))
|
||||
* **core:** Fix XSS validation and separate URL validation ([#10424](https://github.com/n8n-io/n8n/issues/10424)) ([91467ab](https://github.com/n8n-io/n8n/commit/91467ab325e4c71c20c522f3143246d270101626))
|
||||
* **core:** Replace `sanitize-html` with `xss` in XSS validator constraint ([#10479](https://github.com/n8n-io/n8n/issues/10479)) ([5dea51a](https://github.com/n8n-io/n8n/commit/5dea51aad7d9e7ffc676d16f4bbbdecce5876f0b))
|
||||
* **core:** Use class-validator with XSS check for survey answers ([#10490](https://github.com/n8n-io/n8n/issues/10490)) ([547a606](https://github.com/n8n-io/n8n/commit/547a60642ce9e54819d4e600c822d87dabd59b2e))
|
||||
* **core:** Use explicit types in configs to ensure valid decorator metadata ([#10433](https://github.com/n8n-io/n8n/issues/10433)) ([2043daa](https://github.com/n8n-io/n8n/commit/2043daa2570bc04b0b8d41f277901a8cc8a7b98f))
|
||||
* **editor:** Add workflow scopes when initializing workflow ([#10455](https://github.com/n8n-io/n8n/issues/10455)) ([b857c2c](https://github.com/n8n-io/n8n/commit/b857c2cda0a9e4386a540d5e1e741570d9453588))
|
||||
* **editor:** Buffer json chunks in stream response ([#10439](https://github.com/n8n-io/n8n/issues/10439)) ([37797f3](https://github.com/n8n-io/n8n/commit/37797f38d81b12d030ba85034baeb49192ea575c))
|
||||
* **editor:** Fix flaky mapping tests ([#10453](https://github.com/n8n-io/n8n/issues/10453)) ([fc6d413](https://github.com/n8n-io/n8n/commit/fc6d4138d58282f676b32f1a6011b1b6d0184bf2))
|
||||
* **editor:** Fix overflow in AI Assistant chat messages ([#10491](https://github.com/n8n-io/n8n/issues/10491)) ([4a6ca63](https://github.com/n8n-io/n8n/commit/4a6ca632100731f85875c639f2164bf1ef415009))
|
||||
* **editor:** Highlight matching type in filter component ([#10425](https://github.com/n8n-io/n8n/issues/10425)) ([6bca879](https://github.com/n8n-io/n8n/commit/6bca879d4ae30c7f9a35e8d6672de42cf93be727))
|
||||
* **editor:** Show item count in output panel schema view ([#10426](https://github.com/n8n-io/n8n/issues/10426)) ([4dee7cc](https://github.com/n8n-io/n8n/commit/4dee7cc36e5f7768d0b71095b194bf357c92e941))
|
||||
* **editor:** Truncate long data pill labels in schema view ([#10427](https://github.com/n8n-io/n8n/issues/10427)) ([1bf2f4f](https://github.com/n8n-io/n8n/commit/1bf2f4f6171d666391bb3a3a312468bc083446e3))
|
||||
* Filter component - improve errors ([#10456](https://github.com/n8n-io/n8n/issues/10456)) ([61ac0c7](https://github.com/n8n-io/n8n/commit/61ac0c77755210f570b887951fe6bbec1a323811))
|
||||
* **Google Sheets Node:** Better error when column to match on is empty ([#10442](https://github.com/n8n-io/n8n/issues/10442)) ([ce46bf5](https://github.com/n8n-io/n8n/commit/ce46bf516a86d9779f37dd75b0c680d26d88e15d))
|
||||
* **Google Sheets Node:** Update name and hint for useAppend option ([#10443](https://github.com/n8n-io/n8n/issues/10443)) ([c5a0c04](https://github.com/n8n-io/n8n/commit/c5a0c049eaf44419c690d151de42fb0c10bd406e))
|
||||
* **Google Sheets Node:** Update to returnAllMatches option ([#10440](https://github.com/n8n-io/n8n/issues/10440)) ([f7fb02e](https://github.com/n8n-io/n8n/commit/f7fb02e92a756781f8e35bbbfc25d71c12cb70af))
|
||||
* **Invoice Ninja Node:** Fix payment types ([#10462](https://github.com/n8n-io/n8n/issues/10462)) ([129245d](https://github.com/n8n-io/n8n/commit/129245da10be1d645f61e929e40b128bd7813f17))
|
||||
* **n8n Form Trigger Node:** Show basic authentication modal on wrong credentials ([#10423](https://github.com/n8n-io/n8n/issues/10423)) ([0dc3e99](https://github.com/n8n-io/n8n/commit/0dc3e99b26bec45e747d83f383cfe5169d89e6b7))
|
||||
* **OpenAI Node:** Throw node operations error in case of openAi client error ([#10448](https://github.com/n8n-io/n8n/issues/10448)) ([0d3ed46](https://github.com/n8n-io/n8n/commit/0d3ed461996bbad06015c455f133baab6506437f))
|
||||
* Project Viewer always seeing a connection error when testing credentials ([#10417](https://github.com/n8n-io/n8n/issues/10417)) ([613cdd2](https://github.com/n8n-io/n8n/commit/613cdd2ba2c0f224c4857a5fc3eea36dbd683049))
|
||||
* Remove unimplemented Postgres credentials options ([#10461](https://github.com/n8n-io/n8n/issues/10461)) ([17ac784](https://github.com/n8n-io/n8n/commit/17ac7844f29d819b91dfaf90b9fe386d98060c42))
|
||||
* Rename Assistant back ([#10481](https://github.com/n8n-io/n8n/issues/10481)) ([c410aed](https://github.com/n8n-io/n8n/commit/c410aed4c22182943dc80ede63acda00b7898e10))
|
||||
* Require mfa code to change email ([#10354](https://github.com/n8n-io/n8n/issues/10354)) ([39c8e50](https://github.com/n8n-io/n8n/commit/39c8e50ad0513649f5a8cef911b7d6cdd61c2372))
|
||||
* **Respond to Webhook Node:** Fix issue preventing the chat trigger from working ([#9886](https://github.com/n8n-io/n8n/issues/9886)) ([9d6ad88](https://github.com/n8n-io/n8n/commit/9d6ad88c14a88fd0dfcb4f9981e38d19cf5f3067))
|
||||
* Show input names when node has multiple inputs ([#10434](https://github.com/n8n-io/n8n/issues/10434)) ([973956c](https://github.com/n8n-io/n8n/commit/973956cc26c78c329ff6eb6934d4f0a24060c87c))
|
||||
* **Toggl Trigger Node:** Update API version ([#10207](https://github.com/n8n-io/n8n/issues/10207)) ([9bdb1d6](https://github.com/n8n-io/n8n/commit/9bdb1d6dca43fe491c5eb96f093b7eec4509eaff))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **core:** Support bidirectional communication between specific mains and specific workers ([#10377](https://github.com/n8n-io/n8n/issues/10377)) ([d0fc9de](https://github.com/n8n-io/n8n/commit/d0fc9dee0e17211c1ed130b19286e9573c9ebfbd))
|
||||
* **Facebook Graph API Node:** Update node to support API v18 - v20 ([#10419](https://github.com/n8n-io/n8n/issues/10419)) ([e7ee10f](https://github.com/n8n-io/n8n/commit/e7ee10f243663d899d32e61bc6264b4df444e2af))
|
||||
|
||||
|
||||
|
||||
# [1.55.0](https://github.com/n8n-io/n8n/compare/n8n@1.54.0...n8n@1.55.0) (2024-08-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Add better error handling for chat errors ([#10408](https://github.com/n8n-io/n8n/issues/10408)) ([f82b6e4](https://github.com/n8n-io/n8n/commit/f82b6e4ba9bf527b3a4c17872162d9ae124ead0d))
|
||||
* **AI Agent Node:** Fix issues with some tools not populating ([#10406](https://github.com/n8n-io/n8n/issues/10406)) ([51a1edd](https://github.com/n8n-io/n8n/commit/51a1eddbf00393f3881c340cf37cfcca59566c99))
|
||||
* **core:** Account for cancelling an execution with no workers available ([#10343](https://github.com/n8n-io/n8n/issues/10343)) ([b044e78](https://github.com/n8n-io/n8n/commit/b044e783e73a499dbd7532a5d489a782d3d021da))
|
||||
* **core:** Account for owner when filtering by project ID in `GET /workflows` in Public API ([#10379](https://github.com/n8n-io/n8n/issues/10379)) ([5ac65b3](https://github.com/n8n-io/n8n/commit/5ac65b36bcb1351c6233b951f064f60862f790a5))
|
||||
* **core:** Enforce shutdown timer and sequence on `SIGINT` for main ([#10346](https://github.com/n8n-io/n8n/issues/10346)) ([5255793](https://github.com/n8n-io/n8n/commit/5255793afee5653d8356b8e4d2e1009d5cf36164))
|
||||
* **core:** Filter out prototype and constructor lookups in expressions ([#10382](https://github.com/n8n-io/n8n/issues/10382)) ([8e7d29a](https://github.com/n8n-io/n8n/commit/8e7d29ad3c4872b1cc147dfcfe9a864ba916692f))
|
||||
* **core:** Fix duplicate Redis publisher ([#10392](https://github.com/n8n-io/n8n/issues/10392)) ([45813de](https://github.com/n8n-io/n8n/commit/45813debc963096f63cc0aabe82d9d9f853a39d7))
|
||||
* **core:** Fix worker shutdown errors when active executions ([#10353](https://github.com/n8n-io/n8n/issues/10353)) ([e071b73](https://github.com/n8n-io/n8n/commit/e071b73bab34edd4b3e6aef6497514acc504cdc6))
|
||||
* **core:** Prevent XSS in user update endpoints ([#10338](https://github.com/n8n-io/n8n/issues/10338)) ([7898498](https://github.com/n8n-io/n8n/commit/78984986a6b4add89df9743b94c113046f1d5ee8))
|
||||
* **core:** Prevent XSS via static cache dir ([#10339](https://github.com/n8n-io/n8n/issues/10339)) ([4f392b5](https://github.com/n8n-io/n8n/commit/4f392b5e3e0ee166e85a2e060b3ec7fcf145229b))
|
||||
* **core:** Rate limit MFA activation and verification endpoints ([#10330](https://github.com/n8n-io/n8n/issues/10330)) ([b6c47c0](https://github.com/n8n-io/n8n/commit/b6c47c0e3214878d42980d5c9535df52b3984b3c))
|
||||
* **editor:** Connect up new project viewer role to the FE ([#9913](https://github.com/n8n-io/n8n/issues/9913)) ([117e2d9](https://github.com/n8n-io/n8n/commit/117e2d968fcc535f32c583ac8f2dc8a84e8cd6bd))
|
||||
* **editor:** Enable credential sharing between all types of projects ([#10233](https://github.com/n8n-io/n8n/issues/10233)) ([1cf48cc](https://github.com/n8n-io/n8n/commit/1cf48cc3019c1cf27e2f3c9affd18426237e9064))
|
||||
* **editor:** Fix rendering of SVG icons in public chat on iOS ([#10381](https://github.com/n8n-io/n8n/issues/10381)) ([7ab3811](https://github.com/n8n-io/n8n/commit/7ab38114dbf3881afba39287a061446ec4bf0431))
|
||||
* **editor:** Fixing XSS vulnerability in toast messages ([#10329](https://github.com/n8n-io/n8n/issues/10329)) ([38bdd9f](https://github.com/n8n-io/n8n/commit/38bdd9f5d0d9ca06fab1a7e1a3e7a4a648a6a89a))
|
||||
* **editor:** Revert change that hid swagger docs in the ui ([#10350](https://github.com/n8n-io/n8n/issues/10350)) ([bae49d3](https://github.com/n8n-io/n8n/commit/bae49d3198d4bcc27e7996cd4f7be3132becc98e))
|
||||
* **n8n Form Trigger Node:** Fix issue preventing v1 node from working ([#10364](https://github.com/n8n-io/n8n/issues/10364)) ([9b647a9](https://github.com/n8n-io/n8n/commit/9b647a9837434e8b75e3ad754ff5136bb5ac760d))
|
||||
* Require mfa code for password change if its enabled ([#10341](https://github.com/n8n-io/n8n/issues/10341)) ([9d7caac](https://github.com/n8n-io/n8n/commit/9d7caacc699f10962783393925a980ec6f1ca975))
|
||||
* Require mfa code to disable mfa ([#10345](https://github.com/n8n-io/n8n/issues/10345)) ([3384f52](https://github.com/n8n-io/n8n/commit/3384f52a35b835ba1d8633dc94bab0ad6e7023b3))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add Ask assistant behind feature flag ([#9995](https://github.com/n8n-io/n8n/issues/9995)) ([5ed2a77](https://github.com/n8n-io/n8n/commit/5ed2a77740db1f02b27c571f4dfdfa206ebdb19c))
|
||||
* **AI Transform Node:** New node ([#10405](https://github.com/n8n-io/n8n/issues/10405)) ([4d222ac](https://github.com/n8n-io/n8n/commit/4d222ac19d943b69fd9f87abe5e5c5f5141eed8d))
|
||||
* **AI Transform Node:** New node ([#9990](https://github.com/n8n-io/n8n/issues/9990)) ([0de9d56](https://github.com/n8n-io/n8n/commit/0de9d56619ed1c055407353046b8a9ebe78da527))
|
||||
* **core:** Allow overriding npm registry for community packages ([#10325](https://github.com/n8n-io/n8n/issues/10325)) ([33a2703](https://github.com/n8n-io/n8n/commit/33a2703429d9eaa41f72d2e7d2da5be60b6c620f))
|
||||
* **editor:** Add schema view to expression modal ([#9976](https://github.com/n8n-io/n8n/issues/9976)) ([71b6c67](https://github.com/n8n-io/n8n/commit/71b6c671797024d7b516352fa9b7ecda101ce3b2))
|
||||
* **MySQL Node:** Return decimal types as numbers ([#10313](https://github.com/n8n-io/n8n/issues/10313)) ([f744d7c](https://github.com/n8n-io/n8n/commit/f744d7c100be68669d9a3efd0033dd371a3cfaf7))
|
||||
* **Okta Node:** Add Okta Node ([#10278](https://github.com/n8n-io/n8n/issues/10278)) ([5cac0f3](https://github.com/n8n-io/n8n/commit/5cac0f339d649cfe5857d33738210cbc1599370b))
|
||||
|
||||
|
||||
|
||||
# [1.54.0](https://github.com/n8n-io/n8n/compare/n8n@1.53.0...n8n@1.54.0) (2024-08-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core:** Ensure OAuth token data is not stubbed in source control ([#10302](https://github.com/n8n-io/n8n/issues/10302)) ([98115e9](https://github.com/n8n-io/n8n/commit/98115e95df8289a8ec400a570a7f256382f8e286))
|
||||
* **core:** Fix expressions in webhook nodes(Form, Webhook) to access previous node's data ([#10247](https://github.com/n8n-io/n8n/issues/10247)) ([88a1701](https://github.com/n8n-io/n8n/commit/88a170176a3447e7f847e9cf145aeb867b1c5fcf))
|
||||
* **core:** Fix user telemetry bugs ([#10293](https://github.com/n8n-io/n8n/issues/10293)) ([42a0b59](https://github.com/n8n-io/n8n/commit/42a0b594d6ea2527c55a2aa9976c904cf70ecf92))
|
||||
* **core:** Make execution and its data creation atomic ([#10276](https://github.com/n8n-io/n8n/issues/10276)) ([ae50bb9](https://github.com/n8n-io/n8n/commit/ae50bb95a8e5bf1cdbf9483da54b84094b82e260))
|
||||
* **core:** Make OAuth1/OAuth2 callback not require auth ([#10263](https://github.com/n8n-io/n8n/issues/10263)) ([a8e2774](https://github.com/n8n-io/n8n/commit/a8e2774f5382e202556b5506c7788265786aa973))
|
||||
* **core:** Revert transactions until we remove the legacy sqlite driver ([#10299](https://github.com/n8n-io/n8n/issues/10299)) ([1eba7c3](https://github.com/n8n-io/n8n/commit/1eba7c3c763ac5b6b28c1c6fc43fc8c215249292))
|
||||
* **core:** Surface enterprise trial error message ([#10267](https://github.com/n8n-io/n8n/issues/10267)) ([432ac1d](https://github.com/n8n-io/n8n/commit/432ac1da59e173ce4c0f2abbc416743d9953ba70))
|
||||
* **core:** Upgrade tournament to address some XSS vulnerabilities ([#10277](https://github.com/n8n-io/n8n/issues/10277)) ([43ae159](https://github.com/n8n-io/n8n/commit/43ae159ea40c574f8e41bdfd221ab2bf3268eee7))
|
||||
* **core:** VM2 sandbox should not throw on `new Promise` ([#10298](https://github.com/n8n-io/n8n/issues/10298)) ([7e95f9e](https://github.com/n8n-io/n8n/commit/7e95f9e2e40a99871f1b6abcdacb39ac5f857332))
|
||||
* **core:** Webhook and form baseUrl missing ([#10290](https://github.com/n8n-io/n8n/issues/10290)) ([8131d66](https://github.com/n8n-io/n8n/commit/8131d66f8ca1b1da00597a12859ee4372148a0c9))
|
||||
* **editor:** Enable moving resources only if team projects are available by the license ([#10271](https://github.com/n8n-io/n8n/issues/10271)) ([42ba884](https://github.com/n8n-io/n8n/commit/42ba8841c401126c77158a53dc8fcbb45dfce8fd))
|
||||
* **editor:** Fix execution retry button ([#10275](https://github.com/n8n-io/n8n/issues/10275)) ([55f2ffe](https://github.com/n8n-io/n8n/commit/55f2ffe256c91a028cee95c3bbb37a093a1c0f81))
|
||||
* **editor:** Update design system Avatar component to show initials also when only firstName or lastName is given ([#10308](https://github.com/n8n-io/n8n/issues/10308)) ([46bbf09](https://github.com/n8n-io/n8n/commit/46bbf09beacad12472d91786b91d845fe2afb26d))
|
||||
* **editor:** Update tags filter/editor to not show non existing tag as a selectable option ([#10297](https://github.com/n8n-io/n8n/issues/10297)) ([557a76e](https://github.com/n8n-io/n8n/commit/557a76ec2326de72fb7a8b46fc4353f8fd9b591d))
|
||||
* **Invoice Ninja Node:** Fix payment types ([#10196](https://github.com/n8n-io/n8n/issues/10196)) ([c5acbb7](https://github.com/n8n-io/n8n/commit/c5acbb7ec0d24ec9b30c221fa3b2fb615fb9ec7f))
|
||||
* Loop node no input data shown ([#10224](https://github.com/n8n-io/n8n/issues/10224)) ([c8ee852](https://github.com/n8n-io/n8n/commit/c8ee852159207be0cfe2c3e0ee8e7b29d838aa35))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **core:** Allow filtering executions and users by project in Public API ([#10250](https://github.com/n8n-io/n8n/issues/10250)) ([7056e50](https://github.com/n8n-io/n8n/commit/7056e50b006bda665f64ce6234c5c1967891c415))
|
||||
* **core:** Allow transferring credentials in Public API ([#10259](https://github.com/n8n-io/n8n/issues/10259)) ([07d7b24](https://github.com/n8n-io/n8n/commit/07d7b247f02a9d7185beca7817deb779a3d665dd))
|
||||
* **core:** Show sub-node error on the logs pane. Open logs pane on sub-node error ([#10248](https://github.com/n8n-io/n8n/issues/10248)) ([57d1c9a](https://github.com/n8n-io/n8n/commit/57d1c9a99e97308f2f1b8ae05ac3861a835e8e5a))
|
||||
* **core:** Support community packages in scaling-mode ([#10228](https://github.com/n8n-io/n8n/issues/10228)) ([88086a4](https://github.com/n8n-io/n8n/commit/88086a41ff5b804b35aa9d9503dc2d48836fe4ec))
|
||||
* **core:** Support create, delete, edit role for users in Public API ([#10279](https://github.com/n8n-io/n8n/issues/10279)) ([84efbd9](https://github.com/n8n-io/n8n/commit/84efbd9b9c51f536b21a4f969ab607d277bef692))
|
||||
* **core:** Support create, read, update, delete projects in Public API ([#10269](https://github.com/n8n-io/n8n/issues/10269)) ([489ce10](https://github.com/n8n-io/n8n/commit/489ce100634c3af678fb300e9a39d273042542e6))
|
||||
* **editor:** Auto-add LLM chain for new LLM nodes on empty canvas ([#10245](https://github.com/n8n-io/n8n/issues/10245)) ([06419d9](https://github.com/n8n-io/n8n/commit/06419d9483ae916e79aace6d8c17e265b419b15d))
|
||||
* **Elasticsearch Node:** Add bulk operations for Elasticsearch ([#9940](https://github.com/n8n-io/n8n/issues/9940)) ([bf8f848](https://github.com/n8n-io/n8n/commit/bf8f848645dfd31527713a55bd1fc93865327017))
|
||||
* **Lemlist Trigger Node:** Update Trigger events ([#10311](https://github.com/n8n-io/n8n/issues/10311)) ([15f10ec](https://github.com/n8n-io/n8n/commit/15f10ec325cb5eda0f952bed3a5f171dd91bc639))
|
||||
* **MongoDB Node:** Add projection to query options on Find ([#9972](https://github.com/n8n-io/n8n/issues/9972)) ([0a84e0d](https://github.com/n8n-io/n8n/commit/0a84e0d8b047669f5cf023c21383d01c929c5b4f))
|
||||
* **Postgres Chat Memory, Redis Chat Memory, Xata:** Add support for context window length ([#10203](https://github.com/n8n-io/n8n/issues/10203)) ([e3edeaa](https://github.com/n8n-io/n8n/commit/e3edeaa03526f041d15d1099ea91869e38a0decc))
|
||||
* **Stripe Trigger Node:** Add Stripe webhook descriptions based on the workflow ID and name ([#9956](https://github.com/n8n-io/n8n/issues/9956)) ([3433465](https://github.com/n8n-io/n8n/commit/34334651e0e6874736a437a894176bed4590e5a7))
|
||||
* **Webflow Node:** Update to use the v2 API ([#9996](https://github.com/n8n-io/n8n/issues/9996)) ([6d8323f](https://github.com/n8n-io/n8n/commit/6d8323fadea8af04483eb1a873df0cf3ccc2a891))
|
||||
|
||||
|
||||
|
||||
# [1.53.0](https://github.com/n8n-io/n8n/compare/n8n@1.52.0...n8n@1.53.0) (2024-07-31)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Better error message when calling data transformation functions on a null value ([#10210](https://github.com/n8n-io/n8n/issues/10210)) ([1718125](https://github.com/n8n-io/n8n/commit/1718125c6d8589cf24dc8d34f6808dd6f1802691))
|
||||
* **core:** Fix missing successful items on continueErrorOutput with multiple outputs ([#10218](https://github.com/n8n-io/n8n/issues/10218)) ([1a7713e](https://github.com/n8n-io/n8n/commit/1a7713ef263680da43f08b6c8a15aee7a0341493))
|
||||
* **core:** Flush instance stopped event immediately ([#10238](https://github.com/n8n-io/n8n/issues/10238)) ([d6770b5](https://github.com/n8n-io/n8n/commit/d6770b5fcaec6438d677b918aaeb1669ad7424c2))
|
||||
* **core:** Restore log event `n8n.workflow.failed` ([#10253](https://github.com/n8n-io/n8n/issues/10253)) ([3e96b29](https://github.com/n8n-io/n8n/commit/3e96b293329525c9d4b2fcef87b3803e458c8e7f))
|
||||
* **core:** Upgrade @n8n/vm2 to address CVE‑2023‑37466 ([#10265](https://github.com/n8n-io/n8n/issues/10265)) ([2a09a03](https://github.com/n8n-io/n8n/commit/2a09a036d2e916acff7ee50904f1d011a93758e1))
|
||||
* **editor:** Defer `User saved credentials` telemetry event for OAuth credentials ([#10215](https://github.com/n8n-io/n8n/issues/10215)) ([40a5226](https://github.com/n8n-io/n8n/commit/40a5226e24448a4428143e69d80ebc78238365a1))
|
||||
* **editor:** Fix custom API call notice ([#10227](https://github.com/n8n-io/n8n/issues/10227)) ([5b47c8b](https://github.com/n8n-io/n8n/commit/5b47c8b57b25528cd2d6f97bc6d98707d47f35bc))
|
||||
* **editor:** Fix issue with existing credential not opening in HTTP agent tool ([#10167](https://github.com/n8n-io/n8n/issues/10167)) ([906b4c3](https://github.com/n8n-io/n8n/commit/906b4c3c7b2919111cf23eaa12b3c4d507969179))
|
||||
* **editor:** Fix parameter input glitch when there was an error loading remote options ([#10209](https://github.com/n8n-io/n8n/issues/10209)) ([c0e3743](https://github.com/n8n-io/n8n/commit/c0e37439a87105a0e66c8ebced42c06dab30dc5e))
|
||||
* **editor:** Fix workflow execution list scrolling after filter change ([#10226](https://github.com/n8n-io/n8n/issues/10226)) ([7e64358](https://github.com/n8n-io/n8n/commit/7e643589c67adc0218216ec4b89a95f0edfedbee))
|
||||
* **Google BigQuery Node:** Send timeoutMs in query, pagination support ([#10205](https://github.com/n8n-io/n8n/issues/10205)) ([f5722e8](https://github.com/n8n-io/n8n/commit/f5722e8823ccd2bc2b5f43ba3c849797d5690a93))
|
||||
* **Google Sheets Node:** Add column names row if sheet is empty ([#10200](https://github.com/n8n-io/n8n/issues/10200)) ([82eba9f](https://github.com/n8n-io/n8n/commit/82eba9fc5ff49b8e2a9db93c10b253fb67a8c644))
|
||||
* **Google Sheets Node:** Do not insert row_number as a new column, do not checkForSchemaChanges in update operation ([#10201](https://github.com/n8n-io/n8n/issues/10201)) ([5136d10](https://github.com/n8n-io/n8n/commit/5136d10ca3492f92af67d4a1d4abc774419580cc))
|
||||
* **Google Sheets Node:** Fix Google Sheet URL regex ([#10195](https://github.com/n8n-io/n8n/issues/10195)) ([e6fd996](https://github.com/n8n-io/n8n/commit/e6fd996973d4f40facf0ebf1eea3cc26acd0603d))
|
||||
* **HTTP Request Node:** Resolve max pages expression ([#10192](https://github.com/n8n-io/n8n/issues/10192)) ([bfc8e1b](https://github.com/n8n-io/n8n/commit/bfc8e1b56f7714e1f52aae747d58d686b86e60f0))
|
||||
* **LinkedIn Node:** Fix issue with some characters cutting off posts early ([#10185](https://github.com/n8n-io/n8n/issues/10185)) ([361b5e7](https://github.com/n8n-io/n8n/commit/361b5e7c37ba49b68dcf5b8122621aad4d8d96e0))
|
||||
* **Postgres Node:** Expressions in query parameters for Postgres executeQuery operation ([#10217](https://github.com/n8n-io/n8n/issues/10217)) ([519fc4d](https://github.com/n8n-io/n8n/commit/519fc4d75325a80b84cc4dcacf52d6f4c02e3a44))
|
||||
* **Postgres Node:** Option to treat query parameters enclosed in single quotas as text ([#10214](https://github.com/n8n-io/n8n/issues/10214)) ([00ec253](https://github.com/n8n-io/n8n/commit/00ec2533374d3def465efee718592fc4001d5602))
|
||||
* **Read/Write Files from Disk Node:** Notice update in file selector, replace backslashes with forward slashes if windows path ([#10186](https://github.com/n8n-io/n8n/issues/10186)) ([3eac673](https://github.com/n8n-io/n8n/commit/3eac673b17986c5c74bd2adb5ad589ba0ca55319))
|
||||
* **Text Classifier Node:** Use proper documentation URL and respect continueOnFail ([#10216](https://github.com/n8n-io/n8n/issues/10216)) ([452f52c](https://github.com/n8n-io/n8n/commit/452f52c124017e002e86c547ba42b1633b14beed))
|
||||
* **Trello Node:** Use body for POST requests ([#10189](https://github.com/n8n-io/n8n/issues/10189)) ([7775d50](https://github.com/n8n-io/n8n/commit/7775d5059b7f69d9af22e7ad7d12c6cf9092a4e5))
|
||||
* **Wait Node:** Authentication fix ([#10236](https://github.com/n8n-io/n8n/issues/10236)) ([f87854f](https://github.com/n8n-io/n8n/commit/f87854f8db360b7b870583753fcfb4af95adab8c))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Calendly Trigger Node:** Add OAuth Credentials Support ([#10251](https://github.com/n8n-io/n8n/issues/10251)) ([326c983](https://github.com/n8n-io/n8n/commit/326c983915a2c382e32398358e7dcadd022c0b77))
|
||||
* **core:** Allow filtering workflows by project and transferring workflows in Public API ([#10231](https://github.com/n8n-io/n8n/issues/10231)) ([d719899](https://github.com/n8n-io/n8n/commit/d719899223907b20a17883a35e4ef637a3453532))
|
||||
* **editor:** Show new executions as `Queued` in the UI, until they actually start ([#10204](https://github.com/n8n-io/n8n/issues/10204)) ([44728d7](https://github.com/n8n-io/n8n/commit/44728d72423f5549dda09589f4a618ebd80899cb))
|
||||
* **HTTP Request Node:** Add option to disable lowercase headers ([#10154](https://github.com/n8n-io/n8n/issues/10154)) ([5aba69b](https://github.com/n8n-io/n8n/commit/5aba69bcf4d232d9860f3cd9fe57cb8839a2f96f))
|
||||
* **Information Extractor Node:** Add new simplified AI-node for information extraction ([#10149](https://github.com/n8n-io/n8n/issues/10149)) ([3d235b0](https://github.com/n8n-io/n8n/commit/3d235b0b2df756df35ac60e3dcd87ad183a07167))
|
||||
* Introduce Google Cloud Platform as external secrets provider ([#10146](https://github.com/n8n-io/n8n/issues/10146)) ([3ccb9df](https://github.com/n8n-io/n8n/commit/3ccb9df2f902e46f8cbb9c46c0727f29d752a773))
|
||||
* **n8n Form Trigger Node:** Improvements ([#10092](https://github.com/n8n-io/n8n/issues/10092)) ([711b667](https://github.com/n8n-io/n8n/commit/711b667ebefe55740e5eb39f1f0f24ceee10e7b0))
|
||||
* Recovery option for jsonParse helper ([#10182](https://github.com/n8n-io/n8n/issues/10182)) ([d165b33](https://github.com/n8n-io/n8n/commit/d165b33ceac4d24d0fc290bffe63b5f551204e38))
|
||||
* **Sentiment Analysis Node:** Implement Sentiment Analysis node ([#10184](https://github.com/n8n-io/n8n/issues/10184)) ([8ef0a0c](https://github.com/n8n-io/n8n/commit/8ef0a0c58ac2a84aad649ccbe72aa907d005cc44))
|
||||
* **Shopify Node:** Update Shopify API version ([#10155](https://github.com/n8n-io/n8n/issues/10155)) ([e2ee915](https://github.com/n8n-io/n8n/commit/e2ee91569a382bfbf787cf45204c72c821a860a0))
|
||||
* Support create, read, delete variables in Public API ([#10241](https://github.com/n8n-io/n8n/issues/10241)) ([af695eb](https://github.com/n8n-io/n8n/commit/af695ebf934526d926ea87fe87df61aa73d70979))
|
||||
|
||||
|
||||
|
||||
# [1.52.0](https://github.com/n8n-io/n8n/compare/n8n@1.51.0...n8n@1.52.0) (2024-07-24)
|
||||
|
||||
|
||||
|
|
|
@ -95,8 +95,8 @@ development environment ready in minutes.
|
|||
## License
|
||||
|
||||
n8n is [fair-code](https://faircode.io) distributed under the
|
||||
[**Sustainable Use License**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md) and the
|
||||
[**n8n Enterprise License**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE_EE.md).
|
||||
[**Sustainable Use License**](https://github.com/n8n-io/n8n/blob/master/LICENSE.md) and the
|
||||
[**n8n Enterprise License**](https://github.com/n8n-io/n8n/blob/master/LICENSE_EE.md).
|
||||
|
||||
Proprietary licenses are available for enterprise customers. [Get in touch](mailto:license@n8n.io)
|
||||
|
||||
|
|
|
@ -1,41 +1,45 @@
|
|||
import { CredentialsModal, WorkflowPage } from '../pages';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
|
||||
const workflowPage = new WorkflowPage();
|
||||
const credentialsModal = new CredentialsModal();
|
||||
|
||||
export const getHomeButton = () => cy.getByTestId('project-home-menu-item');
|
||||
export const getMenuItems = () => cy.getByTestId('project-menu-item');
|
||||
export const getAddProjectButton = () => cy.getByTestId('add-project-menu-item');
|
||||
export const getAddProjectButton = () =>
|
||||
cy.getByTestId('add-project-menu-item').should('contain', 'Add project').should('be.visible');
|
||||
export const getProjectTabs = () => cy.getByTestId('project-tabs').find('a');
|
||||
export const getProjectTabWorkflows = () => getProjectTabs().filter('a[href$="/workflows"]');
|
||||
export const getProjectTabCredentials = () => getProjectTabs().filter('a[href$="/credentials"]');
|
||||
export const getProjectTabSettings = () => getProjectTabs().filter('a[href$="/settings"]');
|
||||
export const getProjectSettingsNameInput = () => cy.getByTestId('project-settings-name-input');
|
||||
export const getProjectSettingsNameInput = () =>
|
||||
cy.getByTestId('project-settings-name-input').find('input');
|
||||
export const getProjectSettingsSaveButton = () => cy.getByTestId('project-settings-save-button');
|
||||
export const getProjectSettingsCancelButton = () =>
|
||||
cy.getByTestId('project-settings-cancel-button');
|
||||
export const getProjectSettingsDeleteButton = () =>
|
||||
cy.getByTestId('project-settings-delete-button');
|
||||
export const getProjectMembersSelect = () => cy.getByTestId('project-members-select');
|
||||
export const addProjectMember = (email: string) => {
|
||||
export const addProjectMember = (email: string, role?: string) => {
|
||||
getProjectMembersSelect().click();
|
||||
getProjectMembersSelect().get('.el-select-dropdown__item').contains(email.toLowerCase()).click();
|
||||
|
||||
if (role) {
|
||||
cy.getByTestId(`user-list-item-${email}`)
|
||||
.find('[data-test-id="projects-settings-user-role-select"]')
|
||||
.click();
|
||||
getVisibleSelect().find('li').contains(role).click();
|
||||
}
|
||||
};
|
||||
export const getProjectNameInput = () => cy.get('#projectName').find('input');
|
||||
export const getResourceMoveModal = () => cy.getByTestId('project-move-resource-modal');
|
||||
export const getResourceMoveConfirmModal = () =>
|
||||
cy.getByTestId('project-move-resource-confirm-modal');
|
||||
export const getProjectMoveSelect = () => cy.getByTestId('project-move-resource-modal-select');
|
||||
|
||||
export function createProject(name: string) {
|
||||
getAddProjectButton().should('be.visible').click();
|
||||
getAddProjectButton().click();
|
||||
|
||||
getProjectNameInput()
|
||||
.should('be.visible')
|
||||
.should('be.focused')
|
||||
.should('have.value', 'My project')
|
||||
.clear()
|
||||
.type(name);
|
||||
getProjectSettingsNameInput().should('be.visible').clear().type(name);
|
||||
getProjectSettingsSaveButton().click();
|
||||
}
|
||||
|
||||
|
@ -46,7 +50,7 @@ export function createWorkflow(fixtureKey: string, name: string) {
|
|||
workflowPage.actions.zoomToFit();
|
||||
}
|
||||
|
||||
export function createCredential(name: string) {
|
||||
export function createCredential(name: string, closeModal = true) {
|
||||
credentialsModal.getters.newCredentialModal().should('be.visible');
|
||||
credentialsModal.getters.newCredentialTypeSelect().should('be.visible');
|
||||
credentialsModal.getters.newCredentialTypeOption('Notion API').click();
|
||||
|
@ -54,13 +58,8 @@ export function createCredential(name: string) {
|
|||
credentialsModal.getters.connectionParameter('Internal Integration Secret').type('1234567890');
|
||||
credentialsModal.actions.setName(name);
|
||||
credentialsModal.actions.save();
|
||||
credentialsModal.actions.close();
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
createProject: (name: string) => {
|
||||
getAddProjectButton().click();
|
||||
getProjectSettingsNameInput().type(name);
|
||||
getProjectSettingsSaveButton().click();
|
||||
},
|
||||
};
|
||||
if (closeModal) {
|
||||
credentialsModal.actions.close();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ export const INSTANCE_MEMBERS = [
|
|||
export const MANUAL_TRIGGER_NODE_NAME = 'Manual Trigger';
|
||||
export const MANUAL_TRIGGER_NODE_DISPLAY_NAME = 'When clicking ‘Test workflow’';
|
||||
export const MANUAL_CHAT_TRIGGER_NODE_NAME = 'Chat Trigger';
|
||||
export const CHAT_TRIGGER_NODE_DISPLAY_NAME = 'When chat message received';
|
||||
export const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger';
|
||||
export const CODE_NODE_NAME = 'Code';
|
||||
export const SET_NODE_NAME = 'Set';
|
||||
|
@ -57,8 +58,10 @@ export const AI_TOOL_CODE_NODE_NAME = 'Code Tool';
|
|||
export const AI_TOOL_WIKIPEDIA_NODE_NAME = 'Wikipedia';
|
||||
export const AI_TOOL_HTTP_NODE_NAME = 'HTTP Request Tool';
|
||||
export const AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME = 'OpenAI Chat Model';
|
||||
export const AI_MEMORY_POSTGRES_NODE_NAME = 'Postgres Chat Memory';
|
||||
export const AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME = 'Auto-fixing Output Parser';
|
||||
export const WEBHOOK_NODE_NAME = 'Webhook';
|
||||
export const EXECUTE_WORKFLOW_NODE_NAME = 'Execute Workflow';
|
||||
|
||||
export const META_KEY = Cypress.platform === 'darwin' ? 'meta' : 'ctrl';
|
||||
|
||||
|
|
|
@ -11,6 +11,21 @@ describe('Inline expression editor', () => {
|
|||
cy.on('uncaught:exception', (error) => error.name !== 'ExpressionError');
|
||||
});
|
||||
|
||||
describe('Basic UI functionality', () => {
|
||||
it('should open and close inline expression preview', () => {
|
||||
WorkflowPage.actions.zoomToFit();
|
||||
WorkflowPage.actions.openNode('Schedule');
|
||||
WorkflowPage.actions.openInlineExpressionEditor();
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('123');
|
||||
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^123$/);
|
||||
// click outside to close
|
||||
ndv.getters.outputPanel().click();
|
||||
WorkflowPage.getters.inlineExpressionEditorOutput().should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Static data', () => {
|
||||
beforeEach(() => {
|
||||
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
||||
|
|
|
@ -145,7 +145,16 @@ describe('Canvas Actions', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should delete connections by pressing the delete button', () => {
|
||||
it('should delete node by pressing keyboard backspace', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
WorkflowPage.getters.canvasNodeByName(CODE_NODE_NAME).click();
|
||||
cy.get('body').type('{backspace}');
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
||||
});
|
||||
|
||||
it('should delete connections by clicking on the delete button', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
|
|
|
@ -40,11 +40,13 @@ describe('Data mapping', () => {
|
|||
|
||||
ndv.actions.mapDataFromHeader(1, 'value');
|
||||
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.timestamp }}');
|
||||
ndv.getters.inlineExpressionEditorInput().type('{esc}');
|
||||
ndv.getters.parameterExpressionPreview('value').should('include.text', '2024');
|
||||
|
||||
ndv.actions.mapDataFromHeader(2, 'value');
|
||||
ndv.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.should('have.text', "{{ $json.timestamp }} {{ $json['Readable date'] }}");
|
||||
.should('have.text', "{{ $json['Readable date'] }}{{ $json.timestamp }}");
|
||||
});
|
||||
|
||||
it('maps expressions from table json, and resolves value based on hover', () => {
|
||||
|
@ -133,6 +135,7 @@ describe('Data mapping', () => {
|
|||
|
||||
ndv.actions.mapToParameter('value');
|
||||
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.input[0].count }}');
|
||||
ndv.getters.inlineExpressionEditorInput().type('{esc}');
|
||||
ndv.getters.parameterExpressionPreview('value').should('include.text', '0');
|
||||
|
||||
ndv.getters
|
||||
|
@ -145,8 +148,9 @@ describe('Data mapping', () => {
|
|||
ndv.actions.mapToParameter('value');
|
||||
ndv.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.should('have.text', '{{ $json.input[0].count }} {{ $json.input }}');
|
||||
ndv.actions.validateExpressionPreview('value', '0 [object Object]');
|
||||
.should('have.text', '{{ $json.input }}{{ $json.input[0].count }}');
|
||||
ndv.getters.inlineExpressionEditorInput().type('{esc}');
|
||||
ndv.actions.validateExpressionPreview('value', '[object Object]0');
|
||||
});
|
||||
|
||||
it('maps expressions from schema view', () => {
|
||||
|
@ -163,6 +167,7 @@ describe('Data mapping', () => {
|
|||
|
||||
ndv.actions.mapToParameter('value');
|
||||
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.input[0].count }}');
|
||||
ndv.getters.inlineExpressionEditorInput().type('{esc}');
|
||||
ndv.actions.validateExpressionPreview('value', '0');
|
||||
|
||||
ndv.getters.inputDataContainer().find('span').contains('input').realMouseDown();
|
||||
|
@ -170,8 +175,8 @@ describe('Data mapping', () => {
|
|||
ndv.actions.mapToParameter('value');
|
||||
ndv.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.should('have.text', '{{ $json.input[0].count }} {{ $json.input }}');
|
||||
ndv.actions.validateExpressionPreview('value', '0 [object Object]');
|
||||
.should('have.text', '{{ $json.input }}{{ $json.input[0].count }}');
|
||||
ndv.actions.validateExpressionPreview('value', '[object Object]0');
|
||||
});
|
||||
|
||||
it('maps expressions from previous nodes', () => {
|
||||
|
@ -192,6 +197,7 @@ describe('Data mapping', () => {
|
|||
ndv.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.should('have.text', `{{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input[0].count }}`);
|
||||
ndv.getters.inlineExpressionEditorInput().type('{esc}');
|
||||
|
||||
ndv.actions.switchInputMode('Table');
|
||||
ndv.actions.selectInputNode(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
|
@ -200,17 +206,17 @@ describe('Data mapping', () => {
|
|||
.inlineExpressionEditorInput()
|
||||
.should(
|
||||
'have.text',
|
||||
`{{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input[0].count }} {{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input }}`,
|
||||
`{{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input }}{{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input[0].count }}`,
|
||||
);
|
||||
|
||||
ndv.actions.selectInputNode('Set');
|
||||
|
||||
ndv.getters.executingLoader().should('not.exist');
|
||||
ndv.getters.inputDataContainer().should('exist');
|
||||
ndv.actions.validateExpressionPreview('value', '0 [object Object]');
|
||||
ndv.actions.validateExpressionPreview('value', '[object Object]0');
|
||||
|
||||
ndv.getters.inputTbodyCell(2, 0).realHover();
|
||||
ndv.actions.validateExpressionPreview('value', '1 [object Object]');
|
||||
ndv.actions.validateExpressionPreview('value', '[object Object]1');
|
||||
});
|
||||
|
||||
it('maps keys to path', () => {
|
||||
|
@ -271,12 +277,12 @@ describe('Data mapping', () => {
|
|||
|
||||
ndv.actions.typeIntoParameterInput('value', 'fun');
|
||||
ndv.actions.clearParameterInput('value'); // keep focus on param
|
||||
cy.wait(300);
|
||||
|
||||
ndv.getters.inputDataContainer().should('exist').find('span').contains('count').realMouseDown();
|
||||
|
||||
ndv.actions.mapToParameter('value');
|
||||
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.input[0].count }}');
|
||||
ndv.getters.inlineExpressionEditorInput().type('{esc}');
|
||||
ndv.actions.validateExpressionPreview('value', '0');
|
||||
|
||||
ndv.getters.inputDataContainer().find('span').contains('input').realMouseDown();
|
||||
|
@ -284,8 +290,8 @@ describe('Data mapping', () => {
|
|||
ndv.actions.mapToParameter('value');
|
||||
ndv.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.should('have.text', '{{ $json.input[0].count }} {{ $json.input }}');
|
||||
ndv.actions.validateExpressionPreview('value', '0 [object Object]');
|
||||
.should('have.text', '{{ $json.input }}{{ $json.input[0].count }}');
|
||||
ndv.actions.validateExpressionPreview('value', '[object Object]0');
|
||||
});
|
||||
|
||||
it('renders expression preview when a previous node is selected', () => {
|
||||
|
@ -342,4 +348,31 @@ describe('Data mapping', () => {
|
|||
.invoke('css', 'border')
|
||||
.should('include', 'dashed rgb(90, 76, 194)');
|
||||
});
|
||||
|
||||
it('maps expressions to a specific location in the editor', () => {
|
||||
cy.fixture('Test_workflow_3.json').then((data) => {
|
||||
cy.get('body').paste(JSON.stringify(data));
|
||||
});
|
||||
workflowPage.actions.zoomToFit();
|
||||
|
||||
workflowPage.actions.openNode('Set');
|
||||
ndv.actions.typeIntoParameterInput('value', '=');
|
||||
ndv.getters.inlineExpressionEditorInput().find('.cm-content').paste('hello world\n\nnewline');
|
||||
ndv.getters.inlineExpressionEditorInput().type('{esc}');
|
||||
|
||||
ndv.getters.inputDataContainer().should('exist').find('span').contains('count').realMouseDown();
|
||||
ndv.actions.mapToParameter('value');
|
||||
ndv.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.should('have.text', '{{ $json.input[0].count }}hello worldnewline');
|
||||
ndv.getters.inlineExpressionEditorInput().type('{esc}');
|
||||
ndv.actions.validateExpressionPreview('value', '0hello world\n\nnewline');
|
||||
|
||||
ndv.getters.inputDataContainer().find('span').contains('input').realMouseDown();
|
||||
ndv.actions.mapToParameter('value', 'center');
|
||||
|
||||
ndv.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.should('have.text', '{{ $json.input[0].count }}hello world{{ $json.input }}newline');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
WorkflowSharingModal,
|
||||
WorkflowsPage,
|
||||
} from '../pages';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
import { getVisibleDropdown, getVisiblePopper, getVisibleSelect } from '../utils';
|
||||
import * as projects from '../composables/projects';
|
||||
|
||||
/**
|
||||
|
@ -180,7 +180,8 @@ describe('Sharing', { disableAutoLogin: true }, () => {
|
|||
).should('be.visible');
|
||||
|
||||
credentialsModal.getters.usersSelect().click();
|
||||
cy.getByTestId('project-sharing-info')
|
||||
getVisiblePopper()
|
||||
.find('[data-test-id="project-sharing-info"]')
|
||||
.filter(':visible')
|
||||
.should('have.length', 3)
|
||||
.contains(INSTANCE_ADMIN.email)
|
||||
|
@ -192,11 +193,79 @@ describe('Sharing', { disableAutoLogin: true }, () => {
|
|||
credentialsModal.actions.saveSharing();
|
||||
credentialsModal.actions.close();
|
||||
});
|
||||
|
||||
it('credentials should work between team and personal projects', () => {
|
||||
cy.resetDatabase();
|
||||
cy.enableFeature('sharing');
|
||||
cy.enableFeature('advancedPermissions');
|
||||
cy.enableFeature('projectRole:admin');
|
||||
cy.enableFeature('projectRole:editor');
|
||||
cy.changeQuota('maxTeamProjects', -1);
|
||||
|
||||
cy.signinAsOwner();
|
||||
cy.visit('/');
|
||||
|
||||
projects.createProject('Development');
|
||||
|
||||
projects.getHomeButton().click();
|
||||
workflowsPage.getters.newWorkflowButtonCard().click();
|
||||
projects.createWorkflow('Test_workflow_1.json', 'Test workflow');
|
||||
|
||||
projects.getHomeButton().click();
|
||||
projects.getProjectTabCredentials().click();
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
projects.createCredential('Notion API');
|
||||
|
||||
credentialsPage.getters.credentialCard('Notion API').click();
|
||||
credentialsModal.actions.changeTab('Sharing');
|
||||
credentialsModal.getters.usersSelect().click();
|
||||
getVisibleSelect()
|
||||
.find('li')
|
||||
.should('have.length', 4)
|
||||
.filter(':contains("Development")')
|
||||
.should('have.length', 1)
|
||||
.click();
|
||||
credentialsModal.getters.saveButton().click();
|
||||
credentialsModal.actions.close();
|
||||
|
||||
projects.getProjectTabWorkflows().click();
|
||||
workflowsPage.getters.workflowCardActions('Test workflow').click();
|
||||
getVisibleDropdown().find('li').contains('Share').click();
|
||||
|
||||
workflowSharingModal.getters.usersSelect().filter(':visible').click();
|
||||
getVisibleSelect().find('li').should('have.length', 3).first().click();
|
||||
workflowSharingModal.getters.saveButton().click();
|
||||
|
||||
projects.getMenuItems().first().click();
|
||||
workflowsPage.getters.newWorkflowButtonCard().click();
|
||||
projects.createWorkflow('Test_workflow_1.json', 'Test workflow 2');
|
||||
workflowPage.actions.openShareModal();
|
||||
workflowSharingModal.getters.usersSelect().should('not.exist');
|
||||
|
||||
cy.get('body').type('{esc}');
|
||||
|
||||
projects.getMenuItems().first().click();
|
||||
projects.getProjectTabCredentials().click();
|
||||
credentialsPage.getters.createCredentialButton().click();
|
||||
projects.createCredential('Notion API 2', false);
|
||||
credentialsModal.actions.changeTab('Sharing');
|
||||
credentialsModal.getters.usersSelect().click();
|
||||
getVisibleSelect().find('li').should('have.length', 4).first().click();
|
||||
credentialsModal.getters.saveButton().click();
|
||||
credentialsModal.actions.close();
|
||||
|
||||
credentialsPage.getters
|
||||
.credentialCards()
|
||||
.should('have.length', 2)
|
||||
.filter(':contains("Owned by me")')
|
||||
.should('have.length', 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Credential Usage in Cross Shared Workflows', () => {
|
||||
beforeEach(() => {
|
||||
cy.resetDatabase();
|
||||
cy.enableFeature('sharing');
|
||||
cy.enableFeature('advancedPermissions');
|
||||
cy.enableFeature('projectRole:admin');
|
||||
cy.enableFeature('projectRole:editor');
|
||||
|
@ -207,23 +276,18 @@ describe('Credential Usage in Cross Shared Workflows', () => {
|
|||
});
|
||||
|
||||
it('should only show credentials from the same team project', () => {
|
||||
cy.enableFeature('advancedPermissions');
|
||||
cy.enableFeature('projectRole:admin');
|
||||
cy.enableFeature('projectRole:editor');
|
||||
cy.changeQuota('maxTeamProjects', -1);
|
||||
|
||||
// Create a notion credential in the home project
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API');
|
||||
|
||||
// Create a notion credential in one project
|
||||
projects.actions.createProject('Development');
|
||||
projects.createProject('Development');
|
||||
projects.getProjectTabCredentials().click();
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API');
|
||||
|
||||
// Create a notion credential in another project
|
||||
projects.actions.createProject('Test');
|
||||
projects.createProject('Test');
|
||||
projects.getProjectTabCredentials().click();
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API');
|
||||
|
@ -238,10 +302,36 @@ describe('Credential Usage in Cross Shared Workflows', () => {
|
|||
getVisibleSelect().find('li').should('have.length', 2);
|
||||
});
|
||||
|
||||
it('should only show credentials in their personal project for members', () => {
|
||||
// Create a notion credential as the owner
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API');
|
||||
|
||||
// Create another notion credential as the owner, but share it with member
|
||||
// 0
|
||||
credentialsPage.getters.createCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API', false);
|
||||
credentialsModal.actions.changeTab('Sharing');
|
||||
credentialsModal.actions.addUser(INSTANCE_MEMBERS[0].email);
|
||||
credentialsModal.actions.saveSharing();
|
||||
|
||||
// As the member, create a new notion credential and a workflow
|
||||
cy.signinAsMember();
|
||||
cy.visit(credentialsPage.url);
|
||||
credentialsPage.getters.createCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API');
|
||||
cy.visit(workflowsPage.url);
|
||||
workflowsPage.actions.createWorkflowFromCard();
|
||||
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
|
||||
|
||||
// Only the own credential the shared one (+ the 'Create new' option)
|
||||
// should be in the dropdown
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
getVisibleSelect().find('li').should('have.length', 3);
|
||||
});
|
||||
|
||||
it('should only show credentials in their personal project for members if the workflow was shared with them', () => {
|
||||
const workflowName = 'Test workflow';
|
||||
cy.enableFeature('sharing');
|
||||
cy.reload();
|
||||
|
||||
// Create a notion credential as the owner and a workflow that is shared
|
||||
// with member 0
|
||||
|
@ -272,7 +362,6 @@ describe('Credential Usage in Cross Shared Workflows', () => {
|
|||
|
||||
it("should show all credentials from all personal projects the workflow's been shared into for the global owner", () => {
|
||||
const workflowName = 'Test workflow';
|
||||
cy.enableFeature('sharing');
|
||||
|
||||
// As member 1, create a new notion credential. This should not show up.
|
||||
cy.signinAsMember(1);
|
||||
|
@ -317,8 +406,6 @@ describe('Credential Usage in Cross Shared Workflows', () => {
|
|||
});
|
||||
|
||||
it('should show all personal credentials if the global owner owns the workflow', () => {
|
||||
cy.enableFeature('sharing');
|
||||
|
||||
// As member 0, create a new notion credential.
|
||||
cy.signinAsMember();
|
||||
cy.visit(credentialsPage.url);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { WorkflowPage } from '../pages';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
|
||||
const wf = new WorkflowPage();
|
||||
|
||||
|
@ -64,10 +65,26 @@ describe('Workflow tags', () => {
|
|||
it('should detach a tag inline by clicking on dropdown list item', () => {
|
||||
wf.getters.createTagButton().click();
|
||||
wf.actions.addTags(TEST_TAGS);
|
||||
wf.getters.nthTagPill(1).click();
|
||||
wf.getters.workflowTagsContainer().click();
|
||||
wf.getters.tagsInDropdown().filter('.selected').first().click();
|
||||
cy.get('body').click(0, 0);
|
||||
wf.getters.workflowTags().click();
|
||||
wf.getters.tagPills().should('have.length', TEST_TAGS.length - 1);
|
||||
});
|
||||
|
||||
it('should not show non existing tag as a selectable option', () => {
|
||||
const NON_EXISTING_TAG = 'My Test Tag';
|
||||
|
||||
wf.getters.createTagButton().click();
|
||||
wf.actions.addTags(TEST_TAGS);
|
||||
cy.get('body').click(0, 0);
|
||||
wf.getters.workflowTags().click();
|
||||
wf.getters.workflowTagsInput().type(NON_EXISTING_TAG);
|
||||
|
||||
getVisibleSelect()
|
||||
.find('li')
|
||||
.should('have.length', 2)
|
||||
.filter(`:contains("${NON_EXISTING_TAG}")`)
|
||||
.should('not.have.length');
|
||||
});
|
||||
});
|
||||
|
|
21
cypress/e2e/1858-PAY-can-use-context-menu.ts
Normal file
21
cypress/e2e/1858-PAY-can-use-context-menu.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
|
||||
const WorkflowPage = new WorkflowPageClass();
|
||||
|
||||
describe('PAY-1858 context menu', () => {
|
||||
it('can use context menu on saved workflow', () => {
|
||||
WorkflowPage.actions.visit();
|
||||
cy.createFixtureWorkflow('Test_workflow_filter.json', 'test');
|
||||
|
||||
WorkflowPage.getters.canvasNodes().should('have.length', 5);
|
||||
WorkflowPage.actions.deleteNodeFromContextMenu('Then');
|
||||
WorkflowPage.getters.canvasNodes().should('have.length', 4);
|
||||
|
||||
WorkflowPage.actions.hitSaveWorkflow();
|
||||
|
||||
cy.reload();
|
||||
WorkflowPage.getters.canvasNodes().should('have.length', 4);
|
||||
WorkflowPage.actions.deleteNodeFromContextMenu('Code');
|
||||
WorkflowPage.getters.canvasNodes().should('have.length', 3);
|
||||
});
|
||||
});
|
|
@ -275,7 +275,6 @@ describe('Execution', () => {
|
|||
.within(() => cy.get('.fa-check').should('not.exist'));
|
||||
|
||||
successToast().should('be.visible');
|
||||
clearNotifications();
|
||||
|
||||
// Clear execution data
|
||||
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
||||
|
@ -617,4 +616,45 @@ describe('Execution', () => {
|
|||
|
||||
errorToast().should('contain', 'Problem in node ‘Telegram‘');
|
||||
});
|
||||
|
||||
it('should not show pinned data in production execution', () => {
|
||||
cy.createFixtureWorkflow('Execution-pinned-data-check.json');
|
||||
|
||||
workflowPage.getters.zoomToFitButton().click();
|
||||
cy.intercept('PATCH', '/rest/workflows/*').as('workflowActivate');
|
||||
workflowPage.getters.activatorSwitch().click();
|
||||
|
||||
cy.wait('@workflowActivate');
|
||||
cy.get('body').type('{esc}');
|
||||
workflowPage.actions.openNode('Webhook');
|
||||
|
||||
cy.contains('label', 'Production URL').should('be.visible').click();
|
||||
cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite');
|
||||
cy.get('.webhook-url').click();
|
||||
ndv.getters.backToCanvas().click();
|
||||
|
||||
cy.readClipboard().then((url) => {
|
||||
cy.request({
|
||||
method: 'GET',
|
||||
url,
|
||||
}).then((resp) => {
|
||||
expect(resp.status).to.eq(200);
|
||||
});
|
||||
});
|
||||
|
||||
cy.intercept('GET', '/rest/executions/*').as('getExecution');
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
|
||||
cy.wait('@getExecution');
|
||||
executionsTab.getters
|
||||
.workflowExecutionPreviewIframe()
|
||||
.should('be.visible')
|
||||
.its('0.contentDocument.body')
|
||||
.should('not.be.empty')
|
||||
|
||||
.then(cy.wrap)
|
||||
.find('.connection-run-items-label')
|
||||
.filter(':contains("5 items")')
|
||||
.should('have.length', 2);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -112,13 +112,13 @@ describe('Credentials', () => {
|
|||
workflowPage.getters.nodeCredentialsSelect().should('have.length', 2);
|
||||
|
||||
workflowPage.getters.nodeCredentialsSelect().first().click();
|
||||
getVisibleSelect().find('li').last().click();
|
||||
getVisibleSelect().find('li').contains('Create New Credential').click();
|
||||
// This one should show auth type selector
|
||||
credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2);
|
||||
cy.get('body').type('{esc}');
|
||||
|
||||
workflowPage.getters.nodeCredentialsSelect().last().click();
|
||||
getVisibleSelect().find('li').last().click();
|
||||
getVisibleSelect().find('li').contains('Create New Credential').click();
|
||||
// This one should not show auth type selector
|
||||
credentialsModal.getters.credentialsAuthTypeSelector().should('not.exist');
|
||||
});
|
||||
|
|
|
@ -9,179 +9,252 @@ const executionsTab = new WorkflowExecutionsTab();
|
|||
const executionsRefreshInterval = 4000;
|
||||
|
||||
// Test suite for executions tab
|
||||
describe('Current Workflow Executions', () => {
|
||||
beforeEach(() => {
|
||||
workflowPage.actions.visit();
|
||||
cy.createFixtureWorkflow('Test_workflow_4_executions_view.json', 'My test workflow');
|
||||
});
|
||||
describe('Workflow Executions', () => {
|
||||
describe('when workflow is saved', () => {
|
||||
beforeEach(() => {
|
||||
workflowPage.actions.visit();
|
||||
cy.createFixtureWorkflow('Test_workflow_4_executions_view.json', 'My test workflow');
|
||||
});
|
||||
|
||||
it('should render executions tab correctly', () => {
|
||||
createMockExecutions();
|
||||
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
|
||||
it('should render executions tab correctly', () => {
|
||||
createMockExecutions();
|
||||
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
|
||||
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
|
||||
cy.wait(['@getExecutions']);
|
||||
cy.wait(['@getExecutions']);
|
||||
|
||||
executionsTab.getters.executionsList().scrollTo(0, 500).wait(0);
|
||||
executionsTab.getters.executionsList().scrollTo(0, 500).wait(0);
|
||||
|
||||
executionsTab.getters.executionListItems().should('have.length', 11);
|
||||
executionsTab.getters.successfulExecutionListItems().should('have.length', 9);
|
||||
executionsTab.getters.failedExecutionListItems().should('have.length', 2);
|
||||
executionsTab.getters
|
||||
.executionListItems()
|
||||
.first()
|
||||
.invoke('attr', 'class')
|
||||
.should('match', /_active_/);
|
||||
});
|
||||
executionsTab.getters.executionListItems().should('have.length', 11);
|
||||
executionsTab.getters.successfulExecutionListItems().should('have.length', 9);
|
||||
executionsTab.getters.failedExecutionListItems().should('have.length', 2);
|
||||
executionsTab.getters
|
||||
.executionListItems()
|
||||
.first()
|
||||
.invoke('attr', 'class')
|
||||
.should('match', /_active_/);
|
||||
});
|
||||
|
||||
it('should not redirect back to execution tab when request is not done before leaving the page', () => {
|
||||
cy.intercept('GET', '/rest/executions?filter=*');
|
||||
cy.intercept('GET', '/rest/executions/active?filter=*');
|
||||
it('should not redirect back to execution tab when request is not done before leaving the page', () => {
|
||||
cy.intercept('GET', '/rest/executions?filter=*');
|
||||
cy.intercept('GET', '/rest/executions/active?filter=*');
|
||||
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
cy.wait(executionsRefreshInterval);
|
||||
cy.url().should('not.include', '/executions');
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
cy.wait(executionsRefreshInterval);
|
||||
cy.url().should('not.include', '/executions');
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
cy.wait(1000);
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
cy.wait(executionsRefreshInterval);
|
||||
cy.url().should('not.include', '/executions');
|
||||
});
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
cy.wait(executionsRefreshInterval);
|
||||
cy.url().should('not.include', '/executions');
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
cy.wait(executionsRefreshInterval);
|
||||
cy.url().should('not.include', '/executions');
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
cy.wait(1000);
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
cy.wait(executionsRefreshInterval);
|
||||
cy.url().should('not.include', '/executions');
|
||||
});
|
||||
|
||||
it('should not redirect back to execution tab when slow request is not done before leaving the page', () => {
|
||||
const throttleResponse: RouteHandler = async (req) => {
|
||||
return await new Promise((resolve) => {
|
||||
setTimeout(() => resolve(req.continue()), 2000);
|
||||
it('should not redirect back to execution tab when slow request is not done before leaving the page', () => {
|
||||
const throttleResponse: RouteHandler = async (req) => {
|
||||
return await new Promise((resolve) => {
|
||||
setTimeout(() => resolve(req.continue()), 2000);
|
||||
});
|
||||
};
|
||||
|
||||
cy.intercept('GET', '/rest/executions?filter=*', throttleResponse);
|
||||
cy.intercept('GET', '/rest/executions/active?filter=*', throttleResponse);
|
||||
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
cy.wait(executionsRefreshInterval);
|
||||
cy.url().should('not.include', '/executions');
|
||||
});
|
||||
|
||||
it('should error toast when server error message returned without stack trace', () => {
|
||||
executionsTab.actions.createManualExecutions(1);
|
||||
const message = 'Workflow did not finish, possible out-of-memory issue';
|
||||
cy.intercept('GET', '/rest/executions/*', {
|
||||
statusCode: 200,
|
||||
body: executionOutOfMemoryServerResponse,
|
||||
}).as('getExecution');
|
||||
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
cy.wait(['@getExecution']);
|
||||
|
||||
executionsTab.getters
|
||||
.workflowExecutionPreviewIframe()
|
||||
.should('be.visible')
|
||||
.its('0.contentDocument.body') // Access the body of the iframe document
|
||||
.should('not.be.empty') // Ensure the body is not empty
|
||||
|
||||
.then(cy.wrap)
|
||||
.find('.el-notification:has(.el-notification--error)')
|
||||
.should('be.visible')
|
||||
.filter(`:contains("${message}")`)
|
||||
.should('be.visible');
|
||||
});
|
||||
|
||||
it('should show workflow data in executions tab after hard reload and modify name and tags', () => {
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 2);
|
||||
|
||||
workflowPage.getters.workflowTags().click();
|
||||
getVisibleSelect().find('li:contains("Manage tags")').click();
|
||||
cy.get('button:contains("Add new")').click();
|
||||
cy.getByTestId('tags-table').find('input').type('nutag').type('{enter}');
|
||||
cy.get('button:contains("Done")').click();
|
||||
|
||||
cy.reload();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.workflowTags().click();
|
||||
workflowPage.getters.tagsInDropdown().first().should('have.text', 'nutag').click();
|
||||
workflowPage.getters.tagPills().should('have.length', 3);
|
||||
|
||||
let newWorkflowName = 'Renamed workflow';
|
||||
workflowPage.actions.renameWorkflow(newWorkflowName);
|
||||
workflowPage.getters.isWorkflowSaved();
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 3);
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 3);
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 3);
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
|
||||
newWorkflowName = 'New workflow';
|
||||
workflowPage.actions.renameWorkflow(newWorkflowName);
|
||||
workflowPage.getters.isWorkflowSaved();
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
workflowPage.getters.workflowTags().click();
|
||||
workflowPage.getters.tagsDropdown().find('.el-tag__close').first().click();
|
||||
cy.get('body').click(0, 0);
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 2);
|
||||
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 2);
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 2);
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
});
|
||||
|
||||
it('should load items and auto scroll after filter change', () => {
|
||||
createMockExecutions();
|
||||
createMockExecutions();
|
||||
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
|
||||
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
|
||||
cy.wait(['@getExecutions']);
|
||||
|
||||
executionsTab.getters.executionsList().scrollTo(0, 500).wait(0);
|
||||
|
||||
executionsTab.getters.executionListItems().eq(10).click();
|
||||
|
||||
cy.getByTestId('executions-filter-button').click();
|
||||
cy.getByTestId('executions-filter-status-select').should('be.visible').click();
|
||||
getVisibleSelect().find('li:contains("Error")').click();
|
||||
|
||||
executionsTab.getters.executionListItems().should('have.length', 5);
|
||||
executionsTab.getters.successfulExecutionListItems().should('have.length', 1);
|
||||
executionsTab.getters.failedExecutionListItems().should('have.length', 4);
|
||||
|
||||
cy.getByTestId('executions-filter-button').click();
|
||||
cy.getByTestId('executions-filter-status-select').should('be.visible').click();
|
||||
getVisibleSelect().find('li:contains("Success")').click();
|
||||
|
||||
// check if the list is scrolled
|
||||
executionsTab.getters.executionListItems().eq(10).should('be.visible');
|
||||
executionsTab.getters.executionsList().then(($el) => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = $el[0];
|
||||
expect(scrollTop).to.be.greaterThan(0);
|
||||
expect(scrollTop + clientHeight).to.be.lessThan(scrollHeight);
|
||||
|
||||
// scroll to the bottom
|
||||
$el[0].scrollTo(0, scrollHeight);
|
||||
executionsTab.getters.executionListItems().should('have.length', 18);
|
||||
executionsTab.getters.successfulExecutionListItems().should('have.length', 18);
|
||||
executionsTab.getters.failedExecutionListItems().should('have.length', 0);
|
||||
});
|
||||
};
|
||||
|
||||
cy.intercept('GET', '/rest/executions?filter=*', throttleResponse);
|
||||
cy.intercept('GET', '/rest/executions/active?filter=*', throttleResponse);
|
||||
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
cy.wait(executionsRefreshInterval);
|
||||
cy.url().should('not.include', '/executions');
|
||||
cy.getByTestId('executions-filter-button').click();
|
||||
cy.getByTestId('executions-filter-reset-button').should('be.visible').click();
|
||||
executionsTab.getters.executionListItems().eq(11).should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
it('should error toast when server error message returned without stack trace', () => {
|
||||
executionsTab.actions.createManualExecutions(1);
|
||||
const message = 'Workflow did not finish, possible out-of-memory issue';
|
||||
cy.intercept('GET', '/rest/executions/*', {
|
||||
statusCode: 200,
|
||||
body: executionOutOfMemoryServerResponse,
|
||||
}).as('getExecution');
|
||||
describe('when new workflow is not saved', () => {
|
||||
beforeEach(() => {
|
||||
workflowPage.actions.visit();
|
||||
});
|
||||
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
cy.wait(['@getExecution']);
|
||||
it('should open executions tab', () => {
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
executionsTab.getters.executionsSidebar().should('be.visible');
|
||||
executionsTab.getters.executionsEmptyList().should('be.visible');
|
||||
cy.getByTestId('workflow-execution-no-trigger-content').should('be.visible');
|
||||
cy.get('button:contains("Add first step")').should('be.visible').click();
|
||||
|
||||
executionsTab.getters
|
||||
.workflowExecutionPreviewIframe()
|
||||
.should('be.visible')
|
||||
.its('0.contentDocument.body') // Access the body of the iframe document
|
||||
.should('not.be.empty') // Ensure the body is not empty
|
||||
cy.getByTestId('node-creator-item-name')
|
||||
.should('be.visible')
|
||||
.filter(':contains("Trigger")')
|
||||
.click();
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
executionsTab.getters.executionsSidebar().should('be.visible');
|
||||
executionsTab.getters.executionsEmptyList().should('be.visible');
|
||||
cy.getByTestId('workflow-execution-no-content').should('be.visible');
|
||||
|
||||
.then(cy.wrap)
|
||||
.find('.el-notification:has(.el-notification--error)')
|
||||
.should('be.visible')
|
||||
.filter(`:contains("${message}")`)
|
||||
.should('be.visible');
|
||||
});
|
||||
|
||||
it('should show workflow data in executions tab after hard reload and modify name and tags', () => {
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 2);
|
||||
|
||||
workflowPage.getters.workflowTags().click();
|
||||
getVisibleSelect().find('li:contains("Manage tags")').click();
|
||||
cy.get('button:contains("Add new")').click();
|
||||
cy.getByTestId('tags-table').find('input').type('nutag').type('{enter}');
|
||||
cy.get('button:contains("Done")').click();
|
||||
|
||||
cy.reload();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.workflowTags().click();
|
||||
workflowPage.getters.tagsInDropdown().first().should('have.text', 'nutag').click();
|
||||
workflowPage.getters.tagPills().should('have.length', 3);
|
||||
|
||||
let newWorkflowName = 'Renamed workflow';
|
||||
workflowPage.actions.renameWorkflow(newWorkflowName);
|
||||
workflowPage.getters.isWorkflowSaved();
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 3);
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 3);
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 3);
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
|
||||
newWorkflowName = 'New workflow';
|
||||
workflowPage.actions.renameWorkflow(newWorkflowName);
|
||||
workflowPage.getters.isWorkflowSaved();
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
workflowPage.getters.workflowTags().click();
|
||||
workflowPage.getters.tagsDropdown().find('.el-tag__close').first().click();
|
||||
cy.get('body').click(0, 0);
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 2);
|
||||
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 2);
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 2);
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
workflowPage.getters.saveButton().find('button').should('be.enabled').click();
|
||||
workflowPage.getters.isWorkflowSaved();
|
||||
workflowPage.getters.nodeViewRoot().should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -189,9 +262,11 @@ const createMockExecutions = () => {
|
|||
executionsTab.actions.createManualExecutions(5);
|
||||
// Make some failed executions by enabling Code node with syntax error
|
||||
executionsTab.actions.toggleNodeEnabled('Error');
|
||||
workflowPage.getters.disabledNodes().should('have.length', 0);
|
||||
executionsTab.actions.createManualExecutions(2);
|
||||
// Then add some more successful ones
|
||||
executionsTab.actions.toggleNodeEnabled('Error');
|
||||
workflowPage.getters.disabledNodes().should('have.length', 1);
|
||||
executionsTab.actions.createManualExecutions(4);
|
||||
};
|
||||
|
||||
|
|
279
cypress/e2e/233-AI-switch-to-logs-on-error.cy.ts
Normal file
279
cypress/e2e/233-AI-switch-to-logs-on-error.cy.ts
Normal file
|
@ -0,0 +1,279 @@
|
|||
import type { ExecutionError } from 'n8n-workflow/src';
|
||||
import { NDV, WorkflowPage as WorkflowPageClass } from '../pages';
|
||||
import {
|
||||
addLanguageModelNodeToParent,
|
||||
addMemoryNodeToParent,
|
||||
addNodeToCanvas,
|
||||
addToolNodeToParent,
|
||||
navigateToNewWorkflowPage,
|
||||
openNode,
|
||||
} from '../composables/workflow';
|
||||
import {
|
||||
AGENT_NODE_NAME,
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
AI_MEMORY_POSTGRES_NODE_NAME,
|
||||
AI_TOOL_CALCULATOR_NODE_NAME,
|
||||
CHAT_TRIGGER_NODE_DISPLAY_NAME,
|
||||
MANUAL_CHAT_TRIGGER_NODE_NAME,
|
||||
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
|
||||
MANUAL_TRIGGER_NODE_NAME,
|
||||
} from '../constants';
|
||||
import {
|
||||
clickCreateNewCredential,
|
||||
clickExecuteNode,
|
||||
clickGetBackToCanvas,
|
||||
} from '../composables/ndv';
|
||||
import { setCredentialValues } from '../composables/modals/credential-modal';
|
||||
import {
|
||||
closeManualChatModal,
|
||||
getManualChatMessages,
|
||||
getManualChatModalLogs,
|
||||
getManualChatModalLogsEntries,
|
||||
sendManualChatMessage,
|
||||
} from '../composables/modals/chat-modal';
|
||||
import { createMockNodeExecutionData, getVisibleSelect, runMockWorkflowExecution } from '../utils';
|
||||
|
||||
const ndv = new NDV();
|
||||
const WorkflowPage = new WorkflowPageClass();
|
||||
|
||||
function createRunDataWithError(inputMessage: string) {
|
||||
return [
|
||||
createMockNodeExecutionData(MANUAL_CHAT_TRIGGER_NODE_NAME, {
|
||||
jsonData: {
|
||||
main: { input: inputMessage },
|
||||
},
|
||||
}),
|
||||
createMockNodeExecutionData(AI_MEMORY_POSTGRES_NODE_NAME, {
|
||||
jsonData: {
|
||||
ai_memory: {
|
||||
json: {
|
||||
action: 'loadMemoryVariables',
|
||||
values: {
|
||||
input: inputMessage,
|
||||
system_message: 'You are a helpful assistant',
|
||||
formatting_instructions:
|
||||
'IMPORTANT: Always call `format_final_response` to format your final response!',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
inputOverride: {
|
||||
ai_memory: [
|
||||
[
|
||||
{
|
||||
json: {
|
||||
action: 'loadMemoryVariables',
|
||||
values: {
|
||||
input: inputMessage,
|
||||
system_message: 'You are a helpful assistant',
|
||||
formatting_instructions:
|
||||
'IMPORTANT: Always call `format_final_response` to format your final response!',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
error: {
|
||||
message: 'Internal error',
|
||||
timestamp: 1722591723244,
|
||||
name: 'NodeOperationError',
|
||||
description: 'Internal error',
|
||||
context: {},
|
||||
cause: {
|
||||
name: 'error',
|
||||
severity: 'FATAL',
|
||||
code: '3D000',
|
||||
file: 'postinit.c',
|
||||
line: '885',
|
||||
routine: 'InitPostgres',
|
||||
} as unknown as Error,
|
||||
} as ExecutionError,
|
||||
}),
|
||||
createMockNodeExecutionData(AGENT_NODE_NAME, {
|
||||
executionStatus: 'error',
|
||||
error: {
|
||||
level: 'error',
|
||||
tags: {
|
||||
packageName: 'workflow',
|
||||
},
|
||||
context: {},
|
||||
functionality: 'configuration-node',
|
||||
name: 'NodeOperationError',
|
||||
timestamp: 1722591723244,
|
||||
node: {
|
||||
parameters: {
|
||||
notice: '',
|
||||
sessionIdType: 'fromInput',
|
||||
tableName: 'n8n_chat_histories',
|
||||
},
|
||||
id: '6b9141da-0135-4e9d-94d1-2d658cbf48b5',
|
||||
name: 'Postgres Chat Memory',
|
||||
type: '@n8n/n8n-nodes-langchain.memoryPostgresChat',
|
||||
typeVersion: 1,
|
||||
position: [1140, 500],
|
||||
credentials: {
|
||||
postgres: {
|
||||
id: 'RkyZetVpGsSfEAhQ',
|
||||
name: 'Postgres account',
|
||||
},
|
||||
},
|
||||
},
|
||||
messages: ['database "chat11" does not exist'],
|
||||
description: 'Internal error',
|
||||
message: 'Internal error',
|
||||
} as unknown as ExecutionError,
|
||||
metadata: {
|
||||
subRun: [
|
||||
{
|
||||
node: 'Postgres Chat Memory',
|
||||
runIndex: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
function setupTestWorkflow(chatTrigger: boolean = false) {
|
||||
// Setup test workflow with AI Agent, Postgres Memory Node (source of error), Calculator Tool, and OpenAI Chat Model
|
||||
if (chatTrigger) {
|
||||
addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true);
|
||||
} else {
|
||||
addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME, true);
|
||||
}
|
||||
|
||||
addNodeToCanvas(AGENT_NODE_NAME, true);
|
||||
|
||||
if (!chatTrigger) {
|
||||
// Remove chat trigger
|
||||
WorkflowPage.getters
|
||||
.canvasNodeByName(CHAT_TRIGGER_NODE_DISPLAY_NAME)
|
||||
.find('[data-test-id="delete-node-button"]')
|
||||
.click({ force: true });
|
||||
|
||||
// Set manual trigger to output standard pinned data
|
||||
openNode(MANUAL_TRIGGER_NODE_DISPLAY_NAME);
|
||||
ndv.actions.editPinnedData();
|
||||
ndv.actions.savePinnedData();
|
||||
ndv.actions.close();
|
||||
}
|
||||
|
||||
// Calculator is added just to make OpenAI Chat Model work (tools can not be empty with OpenAI model)
|
||||
addToolNodeToParent(AI_TOOL_CALCULATOR_NODE_NAME, AGENT_NODE_NAME);
|
||||
clickGetBackToCanvas();
|
||||
|
||||
addMemoryNodeToParent(AI_MEMORY_POSTGRES_NODE_NAME, AGENT_NODE_NAME);
|
||||
|
||||
clickCreateNewCredential();
|
||||
setCredentialValues({
|
||||
password: 'testtesttest',
|
||||
});
|
||||
|
||||
ndv.getters.parameterInput('sessionIdType').click();
|
||||
getVisibleSelect().contains('Define below').click();
|
||||
ndv.getters.parameterInput('sessionKey').type('asdasd');
|
||||
|
||||
clickGetBackToCanvas();
|
||||
|
||||
addLanguageModelNodeToParent(
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
AGENT_NODE_NAME,
|
||||
true,
|
||||
);
|
||||
|
||||
clickCreateNewCredential();
|
||||
setCredentialValues({
|
||||
apiKey: 'sk_test_123',
|
||||
});
|
||||
clickGetBackToCanvas();
|
||||
|
||||
WorkflowPage.actions.zoomToFit();
|
||||
}
|
||||
|
||||
function checkMessages(inputMessage: string, outputMessage: string) {
|
||||
const messages = getManualChatMessages();
|
||||
messages.should('have.length', 2);
|
||||
messages.should('contain', inputMessage);
|
||||
messages.should('contain', outputMessage);
|
||||
|
||||
getManualChatModalLogs().should('exist');
|
||||
getManualChatModalLogsEntries()
|
||||
.should('have.length', 1)
|
||||
.should('contain', AI_MEMORY_POSTGRES_NODE_NAME);
|
||||
}
|
||||
|
||||
describe("AI-233 Make root node's logs pane active in case of an error in sub-nodes", () => {
|
||||
beforeEach(() => {
|
||||
navigateToNewWorkflowPage();
|
||||
});
|
||||
|
||||
it('should open logs tab by default when there was an error', () => {
|
||||
setupTestWorkflow(true);
|
||||
|
||||
openNode(AGENT_NODE_NAME);
|
||||
|
||||
const inputMessage = 'Test the code tool';
|
||||
|
||||
clickExecuteNode();
|
||||
runMockWorkflowExecution({
|
||||
trigger: () => sendManualChatMessage(inputMessage),
|
||||
runData: createRunDataWithError(inputMessage),
|
||||
lastNodeExecuted: AGENT_NODE_NAME,
|
||||
});
|
||||
|
||||
checkMessages(inputMessage, '[ERROR: Internal error]');
|
||||
closeManualChatModal();
|
||||
|
||||
// Open the AI Agent node to see the logs
|
||||
openNode(AGENT_NODE_NAME);
|
||||
|
||||
// Finally check that logs pane is opened by default
|
||||
ndv.getters.outputDataContainer().should('be.visible');
|
||||
|
||||
ndv.getters.aiOutputModeToggle().should('be.visible');
|
||||
ndv.getters
|
||||
.aiOutputModeToggle()
|
||||
.find('[role="radio"]')
|
||||
.should('have.length', 2)
|
||||
.eq(1)
|
||||
.should('have.attr', 'aria-checked', 'true');
|
||||
|
||||
ndv.getters
|
||||
.outputPanel()
|
||||
.findChildByTestId('node-error-message')
|
||||
.should('be.visible')
|
||||
.should('contain', 'Error in sub-node');
|
||||
});
|
||||
|
||||
it('should switch to logs tab on error, when NDV is already opened', () => {
|
||||
setupTestWorkflow(false);
|
||||
|
||||
openNode(AGENT_NODE_NAME);
|
||||
|
||||
const inputMessage = 'Test the code tool';
|
||||
|
||||
runMockWorkflowExecution({
|
||||
trigger: () => clickExecuteNode(),
|
||||
runData: createRunDataWithError(inputMessage),
|
||||
lastNodeExecuted: AGENT_NODE_NAME,
|
||||
});
|
||||
|
||||
// Check that logs pane is opened by default
|
||||
ndv.getters.outputDataContainer().should('be.visible');
|
||||
|
||||
ndv.getters.aiOutputModeToggle().should('be.visible');
|
||||
ndv.getters
|
||||
.aiOutputModeToggle()
|
||||
.find('[role="radio"]')
|
||||
.should('have.length', 2)
|
||||
.eq(1)
|
||||
.should('have.attr', 'aria-checked', 'true');
|
||||
|
||||
ndv.getters
|
||||
.outputPanel()
|
||||
.findChildByTestId('node-error-message')
|
||||
.should('be.visible')
|
||||
.should('contain', 'Error in sub-node');
|
||||
});
|
||||
});
|
|
@ -10,7 +10,9 @@ import {
|
|||
disableNode,
|
||||
getExecuteWorkflowButton,
|
||||
navigateToNewWorkflowPage,
|
||||
getNodes,
|
||||
openNode,
|
||||
getConnectionBySourceAndTarget,
|
||||
} from '../composables/workflow';
|
||||
import {
|
||||
clickCreateNewCredential,
|
||||
|
@ -41,6 +43,7 @@ import {
|
|||
AI_TOOL_WIKIPEDIA_NODE_NAME,
|
||||
BASIC_LLM_CHAIN_NODE_NAME,
|
||||
EDIT_FIELDS_SET_NODE_NAME,
|
||||
CHAT_TRIGGER_NODE_DISPLAY_NAME,
|
||||
} from './../constants';
|
||||
|
||||
describe('Langchain Integration', () => {
|
||||
|
@ -331,4 +334,27 @@ describe('Langchain Integration', () => {
|
|||
|
||||
closeManualChatModal();
|
||||
});
|
||||
|
||||
it('should auto-add chat trigger and basic LLM chain when adding LLM node', () => {
|
||||
addNodeToCanvas(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, true);
|
||||
|
||||
getConnectionBySourceAndTarget(
|
||||
CHAT_TRIGGER_NODE_DISPLAY_NAME,
|
||||
BASIC_LLM_CHAIN_NODE_NAME,
|
||||
).should('exist');
|
||||
|
||||
getConnectionBySourceAndTarget(
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
BASIC_LLM_CHAIN_NODE_NAME,
|
||||
).should('exist');
|
||||
getNodes().should('have.length', 3);
|
||||
});
|
||||
|
||||
it('should not auto-add nodes if AI nodes are already present', () => {
|
||||
addNodeToCanvas(AGENT_NODE_NAME, true);
|
||||
|
||||
addNodeToCanvas(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, true);
|
||||
getConnectionBySourceAndTarget(CHAT_TRIGGER_NODE_DISPLAY_NAME, AGENT_NODE_NAME).should('exist');
|
||||
getNodes().should('have.length', 3);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -35,13 +35,14 @@ describe('Personal Settings', () => {
|
|||
successToast().find('.el-notification__closeBtn').click();
|
||||
});
|
||||
});
|
||||
// eslint-disable-next-line n8n-local-rules/no-skipped-tests
|
||||
it('not allow malicious values for personal data', () => {
|
||||
cy.visit('/settings/personal');
|
||||
INVALID_NAMES.forEach((name) => {
|
||||
cy.getByTestId('personal-data-form').find('input[name="firstName"]').clear().type(name);
|
||||
cy.getByTestId('personal-data-form').find('input[name="lastName"]').clear().type(name);
|
||||
cy.getByTestId('save-settings-button').click();
|
||||
errorToast().should('contain', 'Malicious firstName | Malicious lastName');
|
||||
errorToast().should('contain', 'Potentially malicious string | Potentially malicious string');
|
||||
errorToast().find('.el-notification__closeBtn').click();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,9 +1,4 @@
|
|||
import {
|
||||
INSTANCE_MEMBERS,
|
||||
INSTANCE_OWNER,
|
||||
MANUAL_TRIGGER_NODE_NAME,
|
||||
NOTION_NODE_NAME,
|
||||
} from '../constants';
|
||||
import { INSTANCE_MEMBERS, MANUAL_TRIGGER_NODE_NAME, NOTION_NODE_NAME } from '../constants';
|
||||
import {
|
||||
WorkflowsPage,
|
||||
WorkflowPage,
|
||||
|
@ -11,9 +6,10 @@ import {
|
|||
CredentialsPage,
|
||||
WorkflowExecutionsTab,
|
||||
NDV,
|
||||
MainSidebar,
|
||||
} from '../pages';
|
||||
import * as projects from '../composables/projects';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
import { getVisibleDropdown, getVisibleModalOverlay, getVisibleSelect } from '../utils';
|
||||
|
||||
const workflowsPage = new WorkflowsPage();
|
||||
const workflowPage = new WorkflowPage();
|
||||
|
@ -21,6 +17,7 @@ const credentialsPage = new CredentialsPage();
|
|||
const credentialsModal = new CredentialsModal();
|
||||
const executionsTab = new WorkflowExecutionsTab();
|
||||
const ndv = new NDV();
|
||||
const mainSidebar = new MainSidebar();
|
||||
|
||||
describe('Projects', { disableAutoLogin: true }, () => {
|
||||
before(() => {
|
||||
|
@ -237,10 +234,30 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
cy.signinAsMember(1);
|
||||
cy.visit(workflowsPage.url);
|
||||
|
||||
projects.getAddProjectButton().should('not.exist');
|
||||
cy.getByTestId('add-project-menu-item').should('not.exist');
|
||||
projects.getMenuItems().should('not.exist');
|
||||
});
|
||||
|
||||
it('should not show viewer role if not licensed', () => {
|
||||
cy.signinAsOwner();
|
||||
cy.visit(workflowsPage.url);
|
||||
|
||||
projects.getMenuItems().first().click();
|
||||
projects.getProjectTabSettings().click();
|
||||
|
||||
cy.get(
|
||||
`[data-test-id="user-list-item-${INSTANCE_MEMBERS[0].email}"] [data-test-id="projects-settings-user-role-select"]`,
|
||||
).click();
|
||||
|
||||
cy.get('.el-select-dropdown__item.is-disabled')
|
||||
.should('contain.text', 'Viewer')
|
||||
.get('span:contains("Upgrade")')
|
||||
.filter(':visible')
|
||||
.click();
|
||||
|
||||
getVisibleModalOverlay().should('contain.text', 'Upgrade to unlock additional roles');
|
||||
});
|
||||
|
||||
describe('when starting from scratch', () => {
|
||||
beforeEach(() => {
|
||||
cy.resetDatabase();
|
||||
|
@ -257,7 +274,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
|
||||
// Create a project and add a credential to it
|
||||
cy.intercept('POST', '/rest/projects').as('projectCreate');
|
||||
projects.getAddProjectButton().should('contain', 'Add project').should('be.visible').click();
|
||||
projects.getAddProjectButton().click();
|
||||
cy.wait('@projectCreate');
|
||||
projects.getMenuItems().should('have.length', 1);
|
||||
projects.getMenuItems().first().click();
|
||||
|
@ -418,7 +435,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
});
|
||||
|
||||
it('should move resources between projects', () => {
|
||||
cy.signin(INSTANCE_OWNER);
|
||||
cy.signinAsOwner();
|
||||
cy.visit(workflowsPage.url);
|
||||
|
||||
// Create a workflow and a credential in the Home project
|
||||
|
@ -563,5 +580,80 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
projects.getProjectTabCredentials().click();
|
||||
credentialsPage.getters.credentialCards().should('have.length', 2);
|
||||
});
|
||||
|
||||
it('should handle viewer role', () => {
|
||||
cy.enableFeature('projectRole:viewer');
|
||||
cy.signinAsOwner();
|
||||
cy.visit(workflowsPage.url);
|
||||
|
||||
projects.createProject('Development');
|
||||
projects.addProjectMember(INSTANCE_MEMBERS[0].email, 'Viewer');
|
||||
projects.getProjectSettingsSaveButton().click();
|
||||
|
||||
projects.getProjectTabWorkflows().click();
|
||||
workflowsPage.getters.newWorkflowButtonCard().click();
|
||||
projects.createWorkflow('Test_workflow_4_executions_view.json', 'WF with random error');
|
||||
executionsTab.actions.createManualExecutions(2);
|
||||
executionsTab.actions.toggleNodeEnabled('Error');
|
||||
executionsTab.actions.createManualExecutions(2);
|
||||
workflowPage.actions.saveWorkflowUsingKeyboardShortcut();
|
||||
|
||||
projects.getMenuItems().first().click();
|
||||
projects.getProjectTabCredentials().click();
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
projects.createCredential('Notion API');
|
||||
|
||||
mainSidebar.actions.openUserMenu();
|
||||
cy.getByTestId('user-menu-item-logout').click();
|
||||
|
||||
cy.get('input[name="email"]').type(INSTANCE_MEMBERS[0].email);
|
||||
cy.get('input[name="password"]').type(INSTANCE_MEMBERS[0].password);
|
||||
cy.getByTestId('form-submit-button').click();
|
||||
|
||||
mainSidebar.getters.executions().click();
|
||||
cy.getByTestId('global-execution-list-item').first().find('td:last button').click();
|
||||
getVisibleDropdown()
|
||||
.find('li')
|
||||
.filter(':contains("Retry")')
|
||||
.should('have.class', 'is-disabled');
|
||||
getVisibleDropdown()
|
||||
.find('li')
|
||||
.filter(':contains("Delete")')
|
||||
.should('have.class', 'is-disabled');
|
||||
|
||||
projects.getMenuItems().first().click();
|
||||
cy.getByTestId('workflow-card-name').should('be.visible').first().click();
|
||||
workflowPage.getters.nodeViewRoot().should('be.visible');
|
||||
workflowPage.getters.executeWorkflowButton().should('not.exist');
|
||||
workflowPage.getters.nodeCreatorPlusButton().should('not.exist');
|
||||
workflowPage.getters.canvasNodes().should('have.length', 3).last().click();
|
||||
cy.get('body').type('{backspace}');
|
||||
workflowPage.getters.canvasNodes().should('have.length', 3).last().rightclick();
|
||||
getVisibleDropdown()
|
||||
.find('li')
|
||||
.should('be.visible')
|
||||
.filter(
|
||||
':contains("Open"), :contains("Copy"), :contains("Select all"), :contains("Clear selection")',
|
||||
)
|
||||
.should('not.have.class', 'is-disabled');
|
||||
cy.get('body').type('{esc}');
|
||||
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
cy.getByTestId('retry-execution-button')
|
||||
.should('be.visible')
|
||||
.find('.is-disabled')
|
||||
.should('exist');
|
||||
cy.get('button:contains("Debug")').should('be.disabled');
|
||||
cy.get('button[title="Retry execution"]').should('be.disabled');
|
||||
cy.get('button[title="Delete this execution"]').should('be.disabled');
|
||||
|
||||
projects.getMenuItems().first().click();
|
||||
projects.getProjectTabCredentials().click();
|
||||
credentialsPage.getters.credentialCards().filter(':contains("Notion")').click();
|
||||
cy.getByTestId('node-credentials-config-container')
|
||||
.should('be.visible')
|
||||
.find('input')
|
||||
.should('not.have.length');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
431
cypress/e2e/45-ai-assistant.cy.ts
Normal file
431
cypress/e2e/45-ai-assistant.cy.ts
Normal file
|
@ -0,0 +1,431 @@
|
|||
import { clickCreateNewCredential, openCredentialSelect } from '../composables/ndv';
|
||||
import { GMAIL_NODE_NAME, SCHEDULE_TRIGGER_NODE_NAME } from '../constants';
|
||||
import { CredentialsModal, CredentialsPage, NDV, WorkflowPage } from '../pages';
|
||||
import { AIAssistant } from '../pages/features/ai-assistant';
|
||||
|
||||
const wf = new WorkflowPage();
|
||||
const ndv = new NDV();
|
||||
const aiAssistant = new AIAssistant();
|
||||
const credentialsPage = new CredentialsPage();
|
||||
const credentialsModal = new CredentialsModal();
|
||||
|
||||
describe('AI Assistant::disabled', () => {
|
||||
beforeEach(() => {
|
||||
aiAssistant.actions.disableAssistant();
|
||||
wf.actions.visit();
|
||||
});
|
||||
|
||||
it('does not show assistant button if feature is disabled', () => {
|
||||
aiAssistant.getters.askAssistantFloatingButton().should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe('AI Assistant::enabled', () => {
|
||||
beforeEach(() => {
|
||||
aiAssistant.actions.enableAssistant();
|
||||
wf.actions.visit();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
aiAssistant.actions.disableAssistant();
|
||||
});
|
||||
|
||||
it('renders placeholder UI', () => {
|
||||
aiAssistant.getters.askAssistantFloatingButton().should('be.visible');
|
||||
aiAssistant.getters.askAssistantFloatingButton().click();
|
||||
aiAssistant.getters.askAssistantChat().should('be.visible');
|
||||
aiAssistant.getters.placeholderMessage().should('be.visible');
|
||||
aiAssistant.getters.chatInput().should('be.visible');
|
||||
aiAssistant.getters.sendMessageButton().should('be.disabled');
|
||||
aiAssistant.getters.closeChatButton().should('be.visible');
|
||||
aiAssistant.getters.closeChatButton().click();
|
||||
aiAssistant.getters.askAssistantChat().should('not.be.visible');
|
||||
});
|
||||
|
||||
it('should resize assistant chat up', () => {
|
||||
aiAssistant.getters.askAssistantFloatingButton().click();
|
||||
aiAssistant.getters.askAssistantSidebarResizer().should('be.visible');
|
||||
aiAssistant.getters.askAssistantChat().then((element) => {
|
||||
const { width, left } = element[0].getBoundingClientRect();
|
||||
cy.drag(aiAssistant.getters.askAssistantSidebarResizer(), [left - 10, 0], {
|
||||
abs: true,
|
||||
clickToFinish: true,
|
||||
});
|
||||
aiAssistant.getters.askAssistantChat().then((newElement) => {
|
||||
const newWidth = newElement[0].getBoundingClientRect().width;
|
||||
expect(newWidth).to.be.greaterThan(width);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should resize assistant chat down', () => {
|
||||
aiAssistant.getters.askAssistantFloatingButton().click();
|
||||
aiAssistant.getters.askAssistantSidebarResizer().should('be.visible');
|
||||
aiAssistant.getters.askAssistantChat().then((element) => {
|
||||
const { width, left } = element[0].getBoundingClientRect();
|
||||
cy.drag(aiAssistant.getters.askAssistantSidebarResizer(), [left + 10, 0], {
|
||||
abs: true,
|
||||
clickToFinish: true,
|
||||
});
|
||||
aiAssistant.getters.askAssistantChat().then((newElement) => {
|
||||
const newWidth = newElement[0].getBoundingClientRect().width;
|
||||
expect(newWidth).to.be.lessThan(width);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should start chat session from node error view', () => {
|
||||
cy.intercept('POST', '/rest/ai-assistant/chat', {
|
||||
statusCode: 200,
|
||||
fixture: 'aiAssistant/simple_message_response.json',
|
||||
}).as('chatRequest');
|
||||
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
|
||||
wf.actions.openNode('Stop and Error');
|
||||
ndv.getters.nodeExecuteButton().click();
|
||||
aiAssistant.getters.nodeErrorViewAssistantButton().click();
|
||||
cy.wait('@chatRequest');
|
||||
aiAssistant.getters.chatMessagesAll().should('have.length', 1);
|
||||
aiAssistant.getters
|
||||
.chatMessagesAll()
|
||||
.eq(0)
|
||||
.should('contain.text', 'Hey, this is an assistant message');
|
||||
aiAssistant.getters.nodeErrorViewAssistantButton().should('be.disabled');
|
||||
});
|
||||
|
||||
it('should render chat input correctly', () => {
|
||||
cy.intercept('POST', '/rest/ai-assistant/chat', {
|
||||
statusCode: 200,
|
||||
fixture: 'aiAssistant/simple_message_response.json',
|
||||
}).as('chatRequest');
|
||||
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
|
||||
wf.actions.openNode('Stop and Error');
|
||||
ndv.getters.nodeExecuteButton().click();
|
||||
aiAssistant.getters.nodeErrorViewAssistantButton().click();
|
||||
cy.wait('@chatRequest');
|
||||
// Send button should be disabled when input is empty
|
||||
aiAssistant.getters.sendMessageButton().should('be.disabled');
|
||||
aiAssistant.getters.chatInput().type('Yo ');
|
||||
aiAssistant.getters.sendMessageButton().should('not.be.disabled');
|
||||
aiAssistant.getters.chatInput().then((element) => {
|
||||
const { height } = element[0].getBoundingClientRect();
|
||||
// Shift + Enter should add a new line
|
||||
aiAssistant.getters.chatInput().type('Hello{shift+enter}there');
|
||||
aiAssistant.getters.chatInput().then((newElement) => {
|
||||
const newHeight = newElement[0].getBoundingClientRect().height;
|
||||
// Chat input should grow as user adds new lines
|
||||
expect(newHeight).to.be.greaterThan(height);
|
||||
aiAssistant.getters.sendMessageButton().click();
|
||||
cy.wait('@chatRequest');
|
||||
// New lines should be rendered as <br> in the chat
|
||||
aiAssistant.getters.chatMessagesUser().should('have.length', 1);
|
||||
aiAssistant.getters.chatMessagesUser().eq(0).find('br').should('have.length', 1);
|
||||
// Chat input should be cleared now
|
||||
aiAssistant.getters.chatInput().should('have.value', '');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should render and handle quick replies', () => {
|
||||
cy.intercept('POST', '/rest/ai-assistant/chat', {
|
||||
statusCode: 200,
|
||||
fixture: 'aiAssistant/quick_reply_message_response.json',
|
||||
}).as('chatRequest');
|
||||
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
|
||||
wf.actions.openNode('Stop and Error');
|
||||
ndv.getters.nodeExecuteButton().click();
|
||||
aiAssistant.getters.nodeErrorViewAssistantButton().click();
|
||||
cy.wait('@chatRequest');
|
||||
aiAssistant.getters.quickReplyButtons().should('have.length', 2);
|
||||
aiAssistant.getters.quickReplyButtons().eq(0).click();
|
||||
cy.wait('@chatRequest');
|
||||
aiAssistant.getters.chatMessagesUser().should('have.length', 1);
|
||||
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-assistant/chat', (req) => {
|
||||
req.reply((res) => {
|
||||
if (['init-error-helper', 'message'].includes(req.body.payload.type)) {
|
||||
res.send({ statusCode: 200, fixture: 'aiAssistant/simple_message_response.json' });
|
||||
} else if (req.body.payload.type === 'event') {
|
||||
res.send({ statusCode: 200, fixture: 'aiAssistant/node_execution_error_response.json' });
|
||||
} else {
|
||||
res.send({ statusCode: 500 });
|
||||
}
|
||||
});
|
||||
}).as('chatRequest');
|
||||
cy.createFixtureWorkflow('aiAssistant/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.chatMessagesAssistant().should('have.length', 4);
|
||||
aiAssistant.getters.quickReplies().should('have.length', 2);
|
||||
});
|
||||
|
||||
it('should warn before starting a new session', () => {
|
||||
cy.intercept('POST', '/rest/ai-assistant/chat', {
|
||||
statusCode: 200,
|
||||
fixture: 'aiAssistant/simple_message_response.json',
|
||||
}).as('chatRequest');
|
||||
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
|
||||
wf.actions.openNode('Edit Fields');
|
||||
ndv.getters.nodeExecuteButton().click();
|
||||
aiAssistant.getters.nodeErrorViewAssistantButton().click({ force: true });
|
||||
cy.wait('@chatRequest');
|
||||
aiAssistant.getters.closeChatButton().click();
|
||||
ndv.getters.backToCanvas().click();
|
||||
wf.actions.openNode('Stop and Error');
|
||||
ndv.getters.nodeExecuteButton().click();
|
||||
aiAssistant.getters.nodeErrorViewAssistantButton().click({ force: true });
|
||||
// Since we already have an active session, a warning should be shown
|
||||
aiAssistant.getters.newAssistantSessionModal().should('be.visible');
|
||||
aiAssistant.getters
|
||||
.newAssistantSessionModal()
|
||||
.find('button')
|
||||
.contains('Start new session')
|
||||
.click();
|
||||
cy.wait('@chatRequest');
|
||||
// New session should start with initial assistant message
|
||||
aiAssistant.getters.chatMessagesAll().should('have.length', 1);
|
||||
});
|
||||
|
||||
it('should apply code diff to code node', () => {
|
||||
cy.intercept('POST', '/rest/ai-assistant/chat', {
|
||||
statusCode: 200,
|
||||
fixture: 'aiAssistant/code_diff_suggestion_response.json',
|
||||
}).as('chatRequest');
|
||||
cy.intercept('POST', '/rest/ai-assistant/chat/apply-suggestion', {
|
||||
statusCode: 200,
|
||||
fixture: 'aiAssistant/apply_code_diff_response.json',
|
||||
}).as('applySuggestion');
|
||||
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
|
||||
wf.actions.openNode('Code');
|
||||
ndv.getters.nodeExecuteButton().click();
|
||||
aiAssistant.getters.nodeErrorViewAssistantButton().click({ force: true });
|
||||
cy.wait('@chatRequest');
|
||||
// Should have two assistant messages
|
||||
aiAssistant.getters.chatMessagesAll().should('have.length', 2);
|
||||
aiAssistant.getters.codeDiffs().should('have.length', 1);
|
||||
aiAssistant.getters.applyCodeDiffButtons().should('have.length', 1);
|
||||
aiAssistant.getters.applyCodeDiffButtons().first().click();
|
||||
cy.wait('@applySuggestion');
|
||||
aiAssistant.getters.applyCodeDiffButtons().should('have.length', 0);
|
||||
aiAssistant.getters.undoReplaceCodeButtons().should('have.length', 1);
|
||||
aiAssistant.getters.codeReplacedMessage().should('be.visible');
|
||||
ndv.getters
|
||||
.parameterInput('jsCode')
|
||||
.get('.cm-content')
|
||||
.should('contain.text', 'item.json.myNewField = 1');
|
||||
// Clicking undo should revert the code back but not call the assistant
|
||||
aiAssistant.getters.undoReplaceCodeButtons().first().click();
|
||||
aiAssistant.getters.applyCodeDiffButtons().should('have.length', 1);
|
||||
aiAssistant.getters.codeReplacedMessage().should('not.exist');
|
||||
cy.get('@applySuggestion.all').then((interceptions) => {
|
||||
expect(interceptions).to.have.length(1);
|
||||
});
|
||||
ndv.getters
|
||||
.parameterInput('jsCode')
|
||||
.get('.cm-content')
|
||||
.should('contain.text', 'item.json.myNewField = 1aaa');
|
||||
// Replacing the code again should also not call the assistant
|
||||
cy.get('@applySuggestion.all').then((interceptions) => {
|
||||
expect(interceptions).to.have.length(1);
|
||||
});
|
||||
aiAssistant.getters.applyCodeDiffButtons().should('have.length', 1);
|
||||
aiAssistant.getters.applyCodeDiffButtons().first().click();
|
||||
ndv.getters
|
||||
.parameterInput('jsCode')
|
||||
.get('.cm-content')
|
||||
.should('contain.text', 'item.json.myNewField = 1');
|
||||
});
|
||||
|
||||
it('should end chat session when `end_session` event is received', () => {
|
||||
cy.intercept('POST', '/rest/ai-assistant/chat', {
|
||||
statusCode: 200,
|
||||
fixture: 'aiAssistant/end_session_response.json',
|
||||
}).as('chatRequest');
|
||||
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
|
||||
wf.actions.openNode('Stop and Error');
|
||||
ndv.getters.nodeExecuteButton().click();
|
||||
aiAssistant.getters.nodeErrorViewAssistantButton().click();
|
||||
cy.wait('@chatRequest');
|
||||
aiAssistant.getters.chatMessagesSystem().should('have.length', 1);
|
||||
aiAssistant.getters.chatMessagesSystem().first().should('contain.text', 'session has ended');
|
||||
});
|
||||
|
||||
it('should reset session after it ended and sidebar is closed', () => {
|
||||
cy.intercept('POST', '/rest/ai-assistant/chat', (req) => {
|
||||
req.reply((res) => {
|
||||
if (['init-support-chat'].includes(req.body.payload.type)) {
|
||||
res.send({ statusCode: 200, fixture: 'aiAssistant/simple_message_response.json' });
|
||||
} else {
|
||||
res.send({ statusCode: 200, fixture: 'aiAssistant/end_session_response.json' });
|
||||
}
|
||||
});
|
||||
}).as('chatRequest');
|
||||
aiAssistant.actions.openChat();
|
||||
aiAssistant.actions.sendMessage('Hello');
|
||||
cy.wait('@chatRequest');
|
||||
aiAssistant.actions.closeChat();
|
||||
aiAssistant.actions.openChat();
|
||||
// After closing and reopening the chat, all messages should be still there
|
||||
aiAssistant.getters.chatMessagesAll().should('have.length', 2);
|
||||
// End the session
|
||||
aiAssistant.actions.sendMessage('Thanks, bye');
|
||||
cy.wait('@chatRequest');
|
||||
aiAssistant.getters.chatMessagesSystem().should('have.length', 1);
|
||||
aiAssistant.getters.chatMessagesSystem().first().should('contain.text', 'session has ended');
|
||||
aiAssistant.actions.closeChat();
|
||||
aiAssistant.actions.openChat();
|
||||
// Now, session should be reset
|
||||
aiAssistant.getters.placeholderMessage().should('be.visible');
|
||||
});
|
||||
|
||||
it('Should not reset assistant session when workflow is saved', () => {
|
||||
cy.intercept('POST', '/rest/ai-assistant/chat', {
|
||||
statusCode: 200,
|
||||
fixture: 'aiAssistant/simple_message_response.json',
|
||||
}).as('chatRequest');
|
||||
wf.actions.addInitialNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
aiAssistant.actions.openChat();
|
||||
aiAssistant.actions.sendMessage('Hello');
|
||||
wf.actions.openNode(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
ndv.getters.nodeExecuteButton().click();
|
||||
wf.getters.isWorkflowSaved();
|
||||
aiAssistant.getters.placeholderMessage().should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe('AI Assistant Credential Help', () => {
|
||||
beforeEach(() => {
|
||||
aiAssistant.actions.enableAssistant();
|
||||
wf.actions.visit();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
aiAssistant.actions.disableAssistant();
|
||||
});
|
||||
|
||||
it('should start credential help from node credential', () => {
|
||||
cy.intercept('POST', '/rest/ai-assistant/chat', {
|
||||
statusCode: 200,
|
||||
fixture: 'aiAssistant/simple_message_response.json',
|
||||
}).as('chatRequest');
|
||||
wf.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
wf.actions.addNodeToCanvas(GMAIL_NODE_NAME);
|
||||
wf.actions.openNode('Gmail');
|
||||
openCredentialSelect();
|
||||
clickCreateNewCredential();
|
||||
aiAssistant.getters.credentialEditAssistantButton().should('be.visible');
|
||||
aiAssistant.getters.credentialEditAssistantButton().click();
|
||||
cy.wait('@chatRequest');
|
||||
aiAssistant.getters.chatMessagesUser().should('have.length', 1);
|
||||
aiAssistant.getters
|
||||
.chatMessagesUser()
|
||||
.eq(0)
|
||||
.should('contain.text', 'How do I set up the credentials for Gmail OAuth2 API?');
|
||||
|
||||
aiAssistant.getters
|
||||
.chatMessagesAssistant()
|
||||
.eq(0)
|
||||
.should('contain.text', 'Hey, this is an assistant message');
|
||||
aiAssistant.getters.credentialEditAssistantButton().should('be.disabled');
|
||||
});
|
||||
|
||||
it('should start credential help from credential list', () => {
|
||||
cy.intercept('POST', '/rest/ai-assistant/chat', {
|
||||
statusCode: 200,
|
||||
fixture: 'aiAssistant/simple_message_response.json',
|
||||
}).as('chatRequest');
|
||||
|
||||
cy.visit(credentialsPage.url);
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
|
||||
credentialsModal.getters.newCredentialModal().should('be.visible');
|
||||
credentialsModal.getters.newCredentialTypeSelect().should('be.visible');
|
||||
credentialsModal.getters.newCredentialTypeOption('Notion API').click();
|
||||
|
||||
credentialsModal.getters.newCredentialTypeButton().click();
|
||||
|
||||
aiAssistant.getters.credentialEditAssistantButton().should('be.visible');
|
||||
aiAssistant.getters.credentialEditAssistantButton().click();
|
||||
cy.wait('@chatRequest');
|
||||
aiAssistant.getters.chatMessagesUser().should('have.length', 1);
|
||||
aiAssistant.getters
|
||||
.chatMessagesUser()
|
||||
.eq(0)
|
||||
.should('contain.text', 'How do I set up the credentials for Notion API?');
|
||||
|
||||
aiAssistant.getters
|
||||
.chatMessagesAssistant()
|
||||
.eq(0)
|
||||
.should('contain.text', 'Hey, this is an assistant message');
|
||||
aiAssistant.getters.credentialEditAssistantButton().should('be.disabled');
|
||||
});
|
||||
});
|
||||
|
||||
describe('General help', () => {
|
||||
beforeEach(() => {
|
||||
aiAssistant.actions.enableAssistant();
|
||||
wf.actions.visit();
|
||||
});
|
||||
|
||||
it('assistant returns code snippet', () => {
|
||||
cy.intercept('POST', '/rest/ai-assistant/chat', {
|
||||
statusCode: 200,
|
||||
fixture: 'aiAssistant/code_snippet_response.json',
|
||||
}).as('chatRequest');
|
||||
|
||||
aiAssistant.getters.askAssistantFloatingButton().should('be.visible');
|
||||
aiAssistant.getters.askAssistantFloatingButton().click();
|
||||
aiAssistant.getters.askAssistantChat().should('be.visible');
|
||||
aiAssistant.getters.placeholderMessage().should('be.visible');
|
||||
aiAssistant.getters.chatInput().should('be.visible');
|
||||
|
||||
aiAssistant.getters.chatInput().type('Show me an expression');
|
||||
aiAssistant.getters.sendMessageButton().click();
|
||||
|
||||
aiAssistant.getters.chatMessagesAll().should('have.length', 3);
|
||||
aiAssistant.getters.chatMessagesUser().eq(0).should('contain.text', 'Show me an expression');
|
||||
|
||||
aiAssistant.getters
|
||||
.chatMessagesAssistant()
|
||||
.eq(0)
|
||||
.should('contain.text', 'To use expressions in n8n, follow these steps:');
|
||||
|
||||
aiAssistant.getters
|
||||
.chatMessagesAssistant()
|
||||
.eq(0)
|
||||
.should(
|
||||
'include.html',
|
||||
`<pre><code class="language-json">[
|
||||
{
|
||||
"headers": {
|
||||
"host": "n8n.instance.address",
|
||||
...
|
||||
},
|
||||
"params": {},
|
||||
"query": {},
|
||||
"body": {
|
||||
"name": "Jim",
|
||||
"age": 30,
|
||||
"city": "New York"
|
||||
}
|
||||
}
|
||||
]
|
||||
</code></pre>`,
|
||||
);
|
||||
aiAssistant.getters.codeSnippet().should('have.text', '{{$json.body.city}}');
|
||||
});
|
||||
});
|
82
cypress/e2e/45-workflow-selector-parameter.cy.ts
Normal file
82
cypress/e2e/45-workflow-selector-parameter.cy.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
import { EXECUTE_WORKFLOW_NODE_NAME } from '../constants';
|
||||
import { WorkflowPage as WorkflowPageClass, NDV } from '../pages';
|
||||
import { getVisiblePopper } from '../utils';
|
||||
|
||||
const workflowPage = new WorkflowPageClass();
|
||||
const ndv = new NDV();
|
||||
|
||||
describe('Workflow Selector Parameter', () => {
|
||||
beforeEach(() => {
|
||||
cy.resetDatabase();
|
||||
cy.signinAsOwner();
|
||||
['Get_Weather', 'Search_DB'].forEach((workflowName) => {
|
||||
workflowPage.actions.visit();
|
||||
cy.createFixtureWorkflow(`Test_Subworkflow_${workflowName}.json`, workflowName);
|
||||
workflowPage.actions.saveWorkflowOnButtonClick();
|
||||
});
|
||||
workflowPage.actions.visit();
|
||||
workflowPage.actions.addInitialNodeToCanvas(EXECUTE_WORKFLOW_NODE_NAME, {
|
||||
keepNdvOpen: true,
|
||||
action: 'Call Another Workflow',
|
||||
});
|
||||
});
|
||||
it('should render sub-workflows list', () => {
|
||||
ndv.getters.resourceLocator('workflowId').should('be.visible');
|
||||
ndv.getters.resourceLocatorInput('workflowId').click();
|
||||
|
||||
getVisiblePopper()
|
||||
.should('have.length', 1)
|
||||
.findChildByTestId('rlc-item')
|
||||
.should('have.length', 2);
|
||||
});
|
||||
|
||||
it('should show required parameter warning', () => {
|
||||
ndv.getters.resourceLocator('workflowId').should('be.visible');
|
||||
ndv.getters.resourceLocatorInput('workflowId').click();
|
||||
ndv.getters.parameterInputIssues('workflowId').should('exist');
|
||||
});
|
||||
|
||||
it('should filter sub-workflows list', () => {
|
||||
ndv.getters.resourceLocator('workflowId').should('be.visible');
|
||||
ndv.getters.resourceLocatorInput('workflowId').click();
|
||||
ndv.getters.resourceLocatorSearch('workflowId').type('Weather');
|
||||
|
||||
getVisiblePopper()
|
||||
.should('have.length', 1)
|
||||
.findChildByTestId('rlc-item')
|
||||
.should('have.length', 1)
|
||||
.click();
|
||||
|
||||
ndv.getters
|
||||
.resourceLocatorInput('workflowId')
|
||||
.find('input')
|
||||
.should('have.value', 'Get_Weather');
|
||||
});
|
||||
|
||||
it('should render sub-workflow links correctly', () => {
|
||||
ndv.getters.resourceLocator('workflowId').should('be.visible');
|
||||
ndv.getters.resourceLocatorInput('workflowId').click();
|
||||
|
||||
getVisiblePopper().findChildByTestId('rlc-item').first().click();
|
||||
|
||||
ndv.getters.resourceLocatorInput('workflowId').find('a').should('exist');
|
||||
cy.getByTestId('radio-button-expression').eq(1).click();
|
||||
ndv.getters.resourceLocatorInput('workflowId').find('a').should('not.exist');
|
||||
});
|
||||
|
||||
it('should switch to ID mode on expression', () => {
|
||||
ndv.getters.resourceLocator('workflowId').should('be.visible');
|
||||
ndv.getters.resourceLocatorInput('workflowId').click();
|
||||
|
||||
getVisiblePopper().findChildByTestId('rlc-item').first().click();
|
||||
ndv.getters
|
||||
.resourceLocatorModeSelector('workflowId')
|
||||
.find('input')
|
||||
.should('have.value', 'From list');
|
||||
cy.getByTestId('radio-button-expression').eq(1).click();
|
||||
ndv.getters
|
||||
.resourceLocatorModeSelector('workflowId')
|
||||
.find('input')
|
||||
.should('have.value', 'By ID');
|
||||
});
|
||||
});
|
35
cypress/e2e/46-n8n.io-iframe.cy.ts
Normal file
35
cypress/e2e/46-n8n.io-iframe.cy.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { WorkflowsPage } from '../pages';
|
||||
|
||||
const workflowsPage = new WorkflowsPage();
|
||||
|
||||
describe('n8n.io iframe', () => {
|
||||
describe('when telemetry is disabled', () => {
|
||||
it('should not load the iframe when visiting /home/workflows', () => {
|
||||
cy.overrideSettings({ telemetry: { enabled: false } });
|
||||
|
||||
cy.visit(workflowsPage.url);
|
||||
|
||||
cy.get('iframe').should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when telemetry is enabled', () => {
|
||||
it('should load the iframe when visiting /home/workflows', () => {
|
||||
const testInstanceId = 'test-instance-id';
|
||||
|
||||
cy.overrideSettings({ telemetry: { enabled: true }, instanceId: testInstanceId });
|
||||
|
||||
const testUserId = Cypress.env('currentUserId');
|
||||
|
||||
const iframeUrl = `https://n8n.io/self-install?instanceId=${testInstanceId}&userId=${testUserId}`;
|
||||
|
||||
cy.intercept(iframeUrl, (req) => req.reply(200)).as('iframeRequest');
|
||||
|
||||
cy.visit(workflowsPage.url);
|
||||
|
||||
cy.get('iframe').should('exist').and('have.attr', 'src', iframeUrl);
|
||||
|
||||
cy.wait('@iframeRequest').its('response.statusCode').should('eq', 200);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -132,6 +132,10 @@ describe('NDV', () => {
|
|||
'contains.text',
|
||||
"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');
|
||||
// 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');
|
||||
});
|
||||
|
||||
it('should save workflow using keyboard shortcut from NDV', () => {
|
||||
|
@ -199,7 +203,7 @@ describe('NDV', () => {
|
|||
.contains(key)
|
||||
.should('be.visible');
|
||||
});
|
||||
getObjectValueItem().find('label').click();
|
||||
getObjectValueItem().find('label').click({ force: true });
|
||||
expandedObjectProps.forEach((key) => {
|
||||
ndv.getters
|
||||
.outputPanel()
|
||||
|
@ -582,7 +586,13 @@ describe('NDV', () => {
|
|||
ndv.getters.outputTableRow(1).find('mark').should('have.text', '<lib');
|
||||
|
||||
ndv.getters.outputDisplayMode().find('label').eq(1).should('include.text', 'JSON');
|
||||
ndv.getters.outputDisplayMode().find('label').eq(1).click();
|
||||
ndv.getters
|
||||
.outputDisplayMode()
|
||||
.find('label')
|
||||
.eq(1)
|
||||
.scrollIntoView()
|
||||
.should('be.visible')
|
||||
.click();
|
||||
|
||||
ndv.getters.outputDataContainer().find('.json-data').should('exist');
|
||||
ndv.getters
|
||||
|
|
|
@ -38,6 +38,51 @@ describe('Code node', () => {
|
|||
|
||||
successToast().contains('Node executed successfully');
|
||||
});
|
||||
|
||||
it('should show lint errors in `runOnceForAllItems` mode', () => {
|
||||
const getParameter = () => ndv.getters.parameterInput('jsCode').should('be.visible');
|
||||
const getEditor = () => getParameter().find('.cm-content').should('exist');
|
||||
|
||||
getEditor().type('{selectall}').paste(`$input.itemMatching()
|
||||
$input.item
|
||||
$('When clicking ‘Test workflow’').item
|
||||
$input.first(1)
|
||||
|
||||
for (const item of $input.all()) {
|
||||
item.foo
|
||||
}
|
||||
|
||||
return
|
||||
`);
|
||||
getParameter().get('.cm-lint-marker-error').should('have.length', 6);
|
||||
getParameter().contains('itemMatching').realHover();
|
||||
cy.get('.cm-tooltip-lint').should(
|
||||
'have.text',
|
||||
'`.itemMatching()` expects an item index to be passed in as its argument.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should show lint errors in `runOnceForEachItem` mode', () => {
|
||||
const getParameter = () => ndv.getters.parameterInput('jsCode').should('be.visible');
|
||||
const getEditor = () => getParameter().find('.cm-content').should('exist');
|
||||
|
||||
ndv.getters.parameterInput('mode').click();
|
||||
ndv.actions.selectOptionInParameterDropdown('mode', 'Run Once for Each Item');
|
||||
getEditor().type('{selectall}').paste(`$input.itemMatching()
|
||||
$input.all()
|
||||
$input.first()
|
||||
$input.item()
|
||||
|
||||
return []
|
||||
`);
|
||||
|
||||
getParameter().get('.cm-lint-marker-error').should('have.length', 5);
|
||||
getParameter().contains('all').realHover();
|
||||
cy.get('.cm-tooltip-lint').should(
|
||||
'have.text',
|
||||
"Method `$input.all()` is only available in the 'Run Once for All Items' mode.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Ask AI', () => {
|
||||
|
|
133
cypress/fixtures/Execution-pinned-data-check.json
Normal file
133
cypress/fixtures/Execution-pinned-data-check.json
Normal file
|
@ -0,0 +1,133 @@
|
|||
{
|
||||
"name": "PAY-1707",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"id": "eaa428a8-eb9d-478a-b997-aed6ed298507",
|
||||
"name": "Edit Fields",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [
|
||||
920,
|
||||
380
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"id": "6b285c91-e7ea-4943-8ba3-59ce01a35d20",
|
||||
"name": "Edit Fields1",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [
|
||||
920,
|
||||
540
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "return Array.from({length: 5}, _ => ({}))"
|
||||
},
|
||||
"id": "70e682aa-dfef-4db7-a158-971ec7976d49",
|
||||
"name": "Code",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
700,
|
||||
380
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "return Array.from({length: 5}, _ => ({}))"
|
||||
},
|
||||
"id": "d5ee979e-9f53-4e62-8eb2-cdb92be8ea6e",
|
||||
"name": "Code1",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
700,
|
||||
540
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"path": "dd660366-ca4a-4736-8b1f-454560e87bfb",
|
||||
"options": {}
|
||||
},
|
||||
"id": "20c33c8a-ab2f-4dd4-990f-6390feeb840c",
|
||||
"name": "Webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
480,
|
||||
440
|
||||
],
|
||||
"webhookId": "dd660366-ca4a-4736-8b1f-454560e87bfb"
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Code1": [
|
||||
{
|
||||
"json": {}
|
||||
},
|
||||
{
|
||||
"json": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"Code1": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Edit Fields1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Code": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Edit Fields",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Code",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Code1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": true,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "01e6693e-54f3-432d-9b1f-922ef92b4ab6",
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": true,
|
||||
"instanceId": "8a47b83b4479b11330fdf21ccc96d4a8117035a968612e452b4c87bfd09c16c7"
|
||||
},
|
||||
"id": "hU0gp19G29ehWktc",
|
||||
"tags": []
|
||||
}
|
53
cypress/fixtures/Test_Subworkflow_Get_Weather.json
Normal file
53
cypress/fixtures/Test_Subworkflow_Get_Weather.json
Normal file
|
@ -0,0 +1,53 @@
|
|||
{
|
||||
"name": "Get Weather",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "82eed1ba-179b-4f8f-8a85-b45f0d4e5857",
|
||||
"name": "Execute Workflow Trigger",
|
||||
"type": "n8n-nodes-base.executeWorkflowTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
560,
|
||||
340
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "6ad8dc55-20f3-45af-a724-c7ecac90d338",
|
||||
"name": "response",
|
||||
"value": "Weather is sunny",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "8f3e00f6-fc92-4aba-817b-93d206158bda",
|
||||
"name": "Edit Fields",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [
|
||||
780,
|
||||
340
|
||||
]
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"Execute Workflow Trigger": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Edit Fields",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
64
cypress/fixtures/Test_Subworkflow_Search_DB.json
Normal file
64
cypress/fixtures/Test_Subworkflow_Search_DB.json
Normal file
|
@ -0,0 +1,64 @@
|
|||
{
|
||||
"name": "Search DB",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "64465f9b-63de-43f9-8d90-b5b2eb7a2dc7",
|
||||
"name": "Execute Workflow Trigger",
|
||||
"type": "n8n-nodes-base.executeWorkflowTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
640,
|
||||
380
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "6ad8dc55-20f3-45af-a724-c7ecac90d338",
|
||||
"name": "response",
|
||||
"value": "10 results found",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "b580fd2b-00c8-4a52-8acb-024f204c0947",
|
||||
"name": "Edit Fields",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [
|
||||
860,
|
||||
380
|
||||
]
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"Execute Workflow Trigger": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Edit Fields",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "6026f7a4-f5dc-4c27-9f83-3a02fc6e33ae",
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": true,
|
||||
"instanceId": "27cc9b56542ad45b38725555722c50a1c3fee1670bbb67980558314ee08517c4"
|
||||
},
|
||||
"id": "BFFhCdBZmNSkx4qf",
|
||||
"tags": []
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"data": {
|
||||
"sessionId": "f9130bd7-c078-4862-a38a-369b27b0ff20-e96eb9f7-d581-4684-b6a9-fd3dfe9fe1fb-emTezIGat7bQsDdtIlbti",
|
||||
"parameters": {
|
||||
"jsCode": "// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n item.json.myNewField = 1;\n}\n\nreturn $input.all();"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"sessionId": "1",
|
||||
"messages": [
|
||||
{
|
||||
"role": "assistant",
|
||||
"type": "message",
|
||||
"text": "Hi there! Here is my top solution to fix the error in your **Code** node 👇"
|
||||
},
|
||||
{
|
||||
"type": "code-diff",
|
||||
"description": "Fix the syntax error by changing '1asd' to a valid value. In this case, it seems like '1' was intended.",
|
||||
"suggestionId": "1",
|
||||
"codeDiff": "@@ -2,2 +2,2 @@\n item.json.myNewField = 1asd;\n+ item.json.myNewField = 1;\n",
|
||||
"role": "assistant",
|
||||
"quickReplies": [
|
||||
{
|
||||
"text": "Give me another solution",
|
||||
"type": "new-suggestion"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
28
cypress/fixtures/aiAssistant/code_snippet_response.json
Normal file
28
cypress/fixtures/aiAssistant/code_snippet_response.json
Normal file
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"sessionId": "f1d19ed5-0d55-4bad-b49a-f0c56bd6f76f-705b5dbf-12d4-4805-87a3-1e5b3c716d29-W1JgVNrpfitpSNF9rAjB4",
|
||||
"messages": [
|
||||
{
|
||||
"role": "assistant",
|
||||
"type": "message",
|
||||
"text": "To use expressions in n8n, follow these steps:\n\n1. Hover over the parameter where you want to use an expression.\n2. Select **Expressions** in the **Fixed/Expression** toggle.\n3. Write your expression in the parameter, or select **Open expression editor** to open the expressions editor. You can browse the available data in the **Variable selector**. All expressions have the format `{{ your expression here }}`.\n\n### Example: Get data from webhook body\n\nIf your webhook data looks like this:\n\n```json\n[\n {\n \"headers\": {\n \"host\": \"n8n.instance.address\",\n ...\n },\n \"params\": {},\n \"query\": {},\n \"body\": {\n \"name\": \"Jim\",\n \"age\": 30,\n \"city\": \"New York\"\n }\n }\n]\n```\n\nYou can use the following expression to get the value of `city`:\n\n```js\n{{$json.body.city}}\n```\n\nThis expression accesses the incoming JSON-formatted data using n8n's custom `$json` variable and finds the value of `city` (in this example, \"New York\").",
|
||||
"codeSnippet": "{{$json.body.city}}"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"type": "message",
|
||||
"text": "Did this answer solve your question?",
|
||||
"quickReplies": [
|
||||
{
|
||||
"text": "Yes, thanks",
|
||||
"type": "all-good",
|
||||
"isFeedback": true
|
||||
},
|
||||
{
|
||||
"text": "No, I am still stuck",
|
||||
"type": "still-stuck",
|
||||
"isFeedback": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
16
cypress/fixtures/aiAssistant/end_session_response.json
Normal file
16
cypress/fixtures/aiAssistant/end_session_response.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"sessionId": "1",
|
||||
"messages": [
|
||||
{
|
||||
"role": "assistant",
|
||||
"type": "message",
|
||||
"title": "Glad to Help",
|
||||
"text": "I'm glad I could help. If you have any more questions or need further assistance with your n8n workflows, feel free to ask!"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"type": "event",
|
||||
"eventName": "end-session"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"sessionId": "1",
|
||||
"messages": [
|
||||
{
|
||||
"role": "assistant",
|
||||
"type": "message",
|
||||
"text": "It seems like my suggestion did not work. Do you want me to come up with a different suggestion? You can also provide more context via the chat.",
|
||||
"quickReplies": [
|
||||
{
|
||||
"text": "Yes",
|
||||
"type": "new-suggestion"
|
||||
},
|
||||
{
|
||||
"text": "No, I don't think you can help",
|
||||
"type": "event:end-session"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"sessionId": "1",
|
||||
"messages": [
|
||||
{
|
||||
"role": "assistant",
|
||||
"type": "message",
|
||||
"text": "Hey, this is an assistant message",
|
||||
"quickReplies": [
|
||||
{
|
||||
"text": "Sure, let's do it",
|
||||
"type": "yes"
|
||||
},
|
||||
{
|
||||
"text": "Nah, doesn't sound good",
|
||||
"type": "no"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
10
cypress/fixtures/aiAssistant/simple_message_response.json
Normal file
10
cypress/fixtures/aiAssistant/simple_message_response.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"sessionId": "1",
|
||||
"messages": [
|
||||
{
|
||||
"role": "assistant",
|
||||
"type": "message",
|
||||
"text": "Hey, this is an assistant message"
|
||||
}
|
||||
]
|
||||
}
|
88
cypress/fixtures/aiAssistant/test_workflow.json
Normal file
88
cypress/fixtures/aiAssistant/test_workflow.json
Normal file
|
@ -0,0 +1,88 @@
|
|||
{
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "ebfced75-2ce1-4c41-a971-6c3b83522c4d",
|
||||
"name": "When clicking ‘Test workflow’",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
360,
|
||||
220
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"errorMessage": "This is an error message"
|
||||
},
|
||||
"id": "f2e60459-401a-49d5-acfc-7b2b31cfdcf7",
|
||||
"name": "Stop and Error",
|
||||
"type": "n8n-nodes-base.stopAndError",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1020,
|
||||
220
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n item.json.myNewField = 1aaa;\n}\n\nreturn $input.all();"
|
||||
},
|
||||
"id": "b54d4db9-b257-41a8-862f-26d293115bad",
|
||||
"name": "Code",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
840,
|
||||
320
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "053ada73-f7db-4e6a-8cc8-85756cc6ca4e",
|
||||
"name": "age",
|
||||
"value": "={{ 32sad }}",
|
||||
"type": "number"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "5fd89612-a871-4679-b7b0-d659e09c6a0e",
|
||||
"name": "Edit Fields",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [
|
||||
600,
|
||||
100
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking ‘Test workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Stop and Error",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Code",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Edit Fields",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {}
|
||||
}
|
|
@ -14,7 +14,7 @@
|
|||
"start": "cd ..; pnpm start"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.14.195",
|
||||
"@types/lodash": "catalog:",
|
||||
"eslint-plugin-cypress": "^3.3.0",
|
||||
"n8n-workflow": "workspace:*"
|
||||
},
|
||||
|
@ -24,8 +24,8 @@
|
|||
"cypress": "^13.11.0",
|
||||
"cypress-otp": "^1.0.3",
|
||||
"cypress-real-events": "^1.12.0",
|
||||
"lodash": "4.17.21",
|
||||
"nanoid": "3.3.6",
|
||||
"start-server-and-test": "^2.0.3"
|
||||
"lodash": "catalog:",
|
||||
"nanoid": "catalog:",
|
||||
"start-server-and-test": "^2.0.5"
|
||||
}
|
||||
}
|
||||
|
|
64
cypress/pages/features/ai-assistant.ts
Normal file
64
cypress/pages/features/ai-assistant.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
import { overrideFeatureFlag } from '../../composables/featureFlags';
|
||||
import { BasePage } from '../base';
|
||||
|
||||
const AI_ASSISTANT_FEATURE = {
|
||||
name: 'aiAssistant',
|
||||
experimentName: '021_ai_debug_helper',
|
||||
enabledFor: 'variant',
|
||||
disabledFor: 'control',
|
||||
};
|
||||
|
||||
export class AIAssistant extends BasePage {
|
||||
url = '/workflows/new';
|
||||
|
||||
getters = {
|
||||
askAssistantFloatingButton: () => cy.getByTestId('ask-assistant-floating-button'),
|
||||
askAssistantSidebar: () => cy.getByTestId('ask-assistant-sidebar'),
|
||||
askAssistantSidebarResizer: () =>
|
||||
this.getters.askAssistantSidebar().find('[class^=_resizer][data-dir=left]').first(),
|
||||
askAssistantChat: () => cy.getByTestId('ask-assistant-chat'),
|
||||
placeholderMessage: () => cy.getByTestId('placeholder-message'),
|
||||
closeChatButton: () => cy.getByTestId('close-chat-button'),
|
||||
chatInputWrapper: () => cy.getByTestId('chat-input-wrapper'),
|
||||
chatInput: () => cy.getByTestId('chat-input'),
|
||||
sendMessageButton: () => cy.getByTestId('send-message-button'),
|
||||
chatMessagesAll: () => cy.get('[data-test-id^=chat-message]'),
|
||||
chatMessagesAssistant: () => cy.getByTestId('chat-message-assistant'),
|
||||
chatMessagesUser: () => cy.getByTestId('chat-message-user'),
|
||||
chatMessagesSystem: () => cy.getByTestId('chat-message-system'),
|
||||
quickReplies: () => cy.getByTestId('quick-replies'),
|
||||
quickReplyButtons: () => this.getters.quickReplies().find('button'),
|
||||
newAssistantSessionModal: () => cy.getByTestId('new-assistant-session-modal'),
|
||||
codeDiffs: () => cy.getByTestId('code-diff-suggestion'),
|
||||
applyCodeDiffButtons: () => cy.getByTestId('replace-code-button'),
|
||||
undoReplaceCodeButtons: () => cy.getByTestId('undo-replace-button'),
|
||||
codeReplacedMessage: () => cy.getByTestId('code-replaced-message'),
|
||||
nodeErrorViewAssistantButton: () =>
|
||||
cy.getByTestId('node-error-view-ask-assistant-button').find('button').first(),
|
||||
credentialEditAssistantButton: () =>
|
||||
cy.getByTestId('credentail-edit-ask-assistant-button').find('button').first(),
|
||||
codeSnippet: () => cy.getByTestId('assistant-code-snippet'),
|
||||
};
|
||||
|
||||
actions = {
|
||||
enableAssistant: () => {
|
||||
overrideFeatureFlag(AI_ASSISTANT_FEATURE.experimentName, AI_ASSISTANT_FEATURE.enabledFor);
|
||||
cy.enableFeature(AI_ASSISTANT_FEATURE.name);
|
||||
},
|
||||
disableAssistant: () => {
|
||||
overrideFeatureFlag(AI_ASSISTANT_FEATURE.experimentName, AI_ASSISTANT_FEATURE.disabledFor);
|
||||
cy.disableFeature(AI_ASSISTANT_FEATURE.name);
|
||||
},
|
||||
sendMessage: (message: string) => {
|
||||
this.getters.chatInput().type(message).type('{enter}');
|
||||
},
|
||||
closeChat: () => {
|
||||
this.getters.closeChatButton().click();
|
||||
this.getters.askAssistantChat().should('not.be.visible');
|
||||
},
|
||||
openChat: () => {
|
||||
this.getters.askAssistantFloatingButton().click();
|
||||
this.getters.askAssistantChat().should('be.visible');
|
||||
},
|
||||
};
|
||||
}
|
|
@ -24,6 +24,7 @@ export class NDV extends BasePage {
|
|||
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'),
|
||||
aiOutputModeToggle: () => cy.getByTestId('ai-output-mode-select'),
|
||||
nodeOutputHint: () => cy.getByTestId('ndv-output-run-node-hint'),
|
||||
savePinnedDataButton: () =>
|
||||
this.getters.runDataPaneHeader().find('button').filter(':visible').contains('Save'),
|
||||
|
@ -137,6 +138,8 @@ export class NDV extends BasePage {
|
|||
cy.getByTestId(`fixed-collection-${paramName}`),
|
||||
schemaViewNode: () => cy.getByTestId('run-data-schema-node'),
|
||||
schemaViewNodeName: () => cy.getByTestId('run-data-schema-node-name'),
|
||||
expressionExpanders: () => cy.getByTestId('expander'),
|
||||
expressionModalOutput: () => cy.getByTestId('expression-modal-output'),
|
||||
};
|
||||
|
||||
actions = {
|
||||
|
@ -174,7 +177,7 @@ export class NDV extends BasePage {
|
|||
this.getters.editPinnedDataButton().click();
|
||||
|
||||
this.getters.pinnedDataEditor().click();
|
||||
this.getters.pinnedDataEditor().type('{selectall}{backspace}').paste(JSON.stringify(data));
|
||||
this.getters.pinnedDataEditor().invoke('text', '').paste(JSON.stringify(data));
|
||||
|
||||
this.actions.savePinnedData();
|
||||
},
|
||||
|
@ -204,9 +207,9 @@ export class NDV extends BasePage {
|
|||
const droppable = `[data-test-id="parameter-input-${parameterName}"]`;
|
||||
cy.draganddrop(draggable, droppable);
|
||||
},
|
||||
mapToParameter: (parameterName: string) => {
|
||||
mapToParameter: (parameterName: string, position?: 'top' | 'center' | 'bottom') => {
|
||||
const droppable = `[data-test-id="parameter-input-${parameterName}"]`;
|
||||
cy.draganddrop('', droppable);
|
||||
cy.draganddrop('', droppable, { position });
|
||||
},
|
||||
switchInputMode: (type: 'Schema' | 'Table' | 'JSON' | 'Binary') => {
|
||||
this.getters.inputDisplayMode().find('label').contains(type).click({ force: true });
|
||||
|
|
|
@ -7,6 +7,7 @@ export class WorkflowExecutionsTab extends BasePage {
|
|||
getters = {
|
||||
executionsTabButton: () => cy.getByTestId('radio-button-executions'),
|
||||
executionsSidebar: () => cy.getByTestId('executions-sidebar'),
|
||||
executionsEmptyList: () => cy.getByTestId('execution-list-empty'),
|
||||
autoRefreshCheckBox: () => cy.getByTestId('auto-refresh-checkbox'),
|
||||
executionsList: () => cy.getByTestId('current-executions-list'),
|
||||
executionListItems: () => this.getters.executionsList().find('div.execution-card'),
|
||||
|
|
|
@ -59,14 +59,18 @@ Cypress.Commands.add('waitForLoad', (waitForIntercepts = true) => {
|
|||
|
||||
Cypress.Commands.add('signin', ({ email, password }) => {
|
||||
void Cypress.session.clearAllSavedSessions();
|
||||
cy.session([email, password], () =>
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: `${BACKEND_BASE_URL}/rest/login`,
|
||||
body: { email, password },
|
||||
failOnStatusCode: false,
|
||||
}),
|
||||
);
|
||||
cy.session([email, password], () => {
|
||||
return cy
|
||||
.request({
|
||||
method: 'POST',
|
||||
url: `${BACKEND_BASE_URL}/rest/login`,
|
||||
body: { email, password },
|
||||
failOnStatusCode: false,
|
||||
})
|
||||
.then((response) => {
|
||||
Cypress.env('currentUserId', response.body.data.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('signinAsOwner', () => cy.signin(INSTANCE_OWNER));
|
||||
|
@ -175,7 +179,7 @@ Cypress.Commands.add('drag', (selector, pos, options) => {
|
|||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('draganddrop', (draggableSelector, droppableSelector) => {
|
||||
Cypress.Commands.add('draganddrop', (draggableSelector, droppableSelector, options) => {
|
||||
if (draggableSelector) {
|
||||
cy.get(draggableSelector).should('exist');
|
||||
}
|
||||
|
@ -197,7 +201,7 @@ Cypress.Commands.add('draganddrop', (draggableSelector, droppableSelector) => {
|
|||
cy.get(droppableSelector).realMouseMove(0, 0);
|
||||
cy.get(droppableSelector).realMouseMove(pageX, pageY);
|
||||
cy.get(droppableSelector).realHover();
|
||||
cy.get(droppableSelector).realMouseUp();
|
||||
cy.get(droppableSelector).realMouseUp({ position: options?.position ?? 'top' });
|
||||
if (draggableSelector) {
|
||||
cy.get(draggableSelector).realMouseUp();
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@ beforeEach(() => {
|
|||
|
||||
cy.window().then((win): void => {
|
||||
win.localStorage.setItem('N8N_THEME', 'light');
|
||||
win.localStorage.setItem('N8N_AUTOCOMPLETE_ONBOARDED', 'true');
|
||||
win.localStorage.setItem('N8N_MAPPING_ONBOARDED', 'true');
|
||||
});
|
||||
|
||||
cy.intercept('GET', '/rest/settings', (req) => {
|
||||
|
|
|
@ -12,6 +12,10 @@ interface SigninPayload {
|
|||
password: string;
|
||||
}
|
||||
|
||||
interface DragAndDropOptions {
|
||||
position: 'top' | 'center' | 'bottom';
|
||||
}
|
||||
|
||||
declare global {
|
||||
namespace Cypress {
|
||||
interface SuiteConfigOverrides {
|
||||
|
@ -56,7 +60,11 @@ declare global {
|
|||
target: [number, number],
|
||||
options?: { abs?: boolean; index?: number; realMouse?: boolean; clickToFinish?: boolean },
|
||||
): void;
|
||||
draganddrop(draggableSelector: string, droppableSelector: string): void;
|
||||
draganddrop(
|
||||
draggableSelector: string,
|
||||
droppableSelector: string,
|
||||
options?: Partial<DragAndDropOptions>,
|
||||
): void;
|
||||
push(type: string, data: unknown): void;
|
||||
shouldNotHaveConsoleErrors(): void;
|
||||
window(): Chainable<
|
||||
|
|
|
@ -3,7 +3,7 @@ export function getPopper() {
|
|||
}
|
||||
|
||||
export function getVisiblePopper() {
|
||||
return getPopper().filter(':visible');
|
||||
return getPopper().filter('[aria-hidden="false"]');
|
||||
}
|
||||
|
||||
export function getVisibleSelect() {
|
||||
|
|
|
@ -230,6 +230,4 @@ Before you upgrade to the latest version make sure to check here if there are an
|
|||
|
||||
## License
|
||||
|
||||
n8n is [fair-code](https://faircode.io) distributed under the [**Sustainable Use License**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md).
|
||||
|
||||
Additional information about the license can be found in the [docs](https://docs.n8n.io/reference/license/).
|
||||
You can find the license information [here](https://github.com/n8n-io/n8n/blob/master/README.md#license)
|
||||
|
|
20
package.json
20
package.json
|
@ -1,22 +1,23 @@
|
|||
{
|
||||
"name": "n8n-monorepo",
|
||||
"version": "1.52.0",
|
||||
"version": "1.59.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=18.10",
|
||||
"pnpm": ">=9.1"
|
||||
"node": ">=20.15",
|
||||
"pnpm": ">=9.5"
|
||||
},
|
||||
"packageManager": "pnpm@9.1.4",
|
||||
"packageManager": "pnpm@9.6.0",
|
||||
"scripts": {
|
||||
"preinstall": "node scripts/block-npm-install.js",
|
||||
"build": "turbo run build",
|
||||
"build:backend": "turbo run build:backend",
|
||||
"build:frontend": "turbo run build:frontend",
|
||||
"build:nodes": "turbo run build:nodes",
|
||||
"typecheck": "turbo --filter=!n8n typecheck",
|
||||
"typecheck": "turbo typecheck",
|
||||
"dev": "turbo run dev --parallel --env-mode=loose --filter=!n8n-design-system --filter=!@n8n/chat",
|
||||
"dev:ai": "turbo run dev --parallel --env-mode=loose --filter=@n8n/nodes-langchain --filter=n8n --filter=n8n-core",
|
||||
"clean": "turbo run clean --parallel",
|
||||
"reset": "node scripts/ensure-zx.mjs && zx scripts/reset.mjs",
|
||||
"format": "turbo run format && node scripts/format.mjs",
|
||||
"lint": "turbo run lint",
|
||||
"lintfix": "turbo run lintfix",
|
||||
|
@ -40,7 +41,6 @@
|
|||
"@n8n_io/eslint-config": "workspace:*",
|
||||
"@types/jest": "^29.5.3",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@vitest/coverage-v8": "^1.6.0",
|
||||
"jest": "^29.6.2",
|
||||
"jest-environment-jsdom": "^29.6.2",
|
||||
"jest-expect-message": "^1.1.3",
|
||||
|
@ -57,10 +57,7 @@
|
|||
"tsc-watch": "^6.0.4",
|
||||
"turbo": "2.0.6",
|
||||
"typescript": "*",
|
||||
"vite": "^5.2.12",
|
||||
"vitest": "^1.6.0",
|
||||
"vitest-mock-extended": "^1.3.1",
|
||||
"vue-tsc": "^2.0.19"
|
||||
"zx": "^8.1.4"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
|
@ -68,7 +65,6 @@
|
|||
],
|
||||
"overrides": {
|
||||
"@types/node": "^18.16.16",
|
||||
"axios": "1.6.7",
|
||||
"chokidar": "3.5.2",
|
||||
"esbuild": "^0.20.2",
|
||||
"formidable": "3.5.1",
|
||||
|
@ -77,7 +73,7 @@
|
|||
"semver": "^7.5.4",
|
||||
"tslib": "^2.6.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.5.2",
|
||||
"typescript": "^5.6.2",
|
||||
"ws": ">=8.17.1"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
|
|
7
packages/@n8n/api-types/.eslintrc.js
Normal file
7
packages/@n8n/api-types/.eslintrc.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
const sharedOptions = require('@n8n_io/eslint-config/shared');
|
||||
|
||||
/** @type {import('@types/eslint').ESLint.ConfigData} */
|
||||
module.exports = {
|
||||
extends: ['@n8n_io/eslint-config/base'],
|
||||
...sharedOptions(__dirname),
|
||||
};
|
3
packages/@n8n/api-types/README.md
Normal file
3
packages/@n8n/api-types/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
## @n8n/api-types
|
||||
|
||||
This package contains types and schema definitions for the n8n internal API, so that these can be shared between the backend and the frontend code.
|
2
packages/@n8n/api-types/jest.config.js
Normal file
2
packages/@n8n/api-types/jest.config.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
/** @type {import('jest').Config} */
|
||||
module.exports = require('../../../jest.config');
|
24
packages/@n8n/api-types/package.json
Normal file
24
packages/@n8n/api-types/package.json
Normal file
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "@n8n/api-types",
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"format": "prettier --write . --ignore-path ../../../.prettierignore",
|
||||
"lint": "eslint .",
|
||||
"lintfix": "eslint . --fix",
|
||||
"watch": "tsc -p tsconfig.build.json --watch",
|
||||
"test": "echo \"No tests yet\" && exit 0"
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"module": "src/index.ts",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist/**/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
"n8n-workflow": "workspace:*"
|
||||
}
|
||||
}
|
2
packages/@n8n/api-types/src/datetime.ts
Normal file
2
packages/@n8n/api-types/src/datetime.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
/** Date time in the ISO 8601 format, e.g. 2024-10-31T00:00:00.123Z */
|
||||
export type Iso8601DateTimeString = string;
|
7
packages/@n8n/api-types/src/index.ts
Normal file
7
packages/@n8n/api-types/src/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export type * from './push';
|
||||
export type * from './scaling';
|
||||
export type * from './datetime';
|
||||
export type * from './user';
|
||||
|
||||
export type { Collaborator } from './push/collaboration';
|
||||
export type { SendWorkerStatusMessage } from './push/worker';
|
17
packages/@n8n/api-types/src/push/collaboration.ts
Normal file
17
packages/@n8n/api-types/src/push/collaboration.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import type { Iso8601DateTimeString } from '../datetime';
|
||||
import type { MinimalUser } from '../user';
|
||||
|
||||
export type Collaborator = {
|
||||
user: MinimalUser;
|
||||
lastSeen: Iso8601DateTimeString;
|
||||
};
|
||||
|
||||
type CollaboratorsChanged = {
|
||||
type: 'collaboratorsChanged';
|
||||
data: {
|
||||
workflowId: string;
|
||||
collaborators: Collaborator[];
|
||||
};
|
||||
};
|
||||
|
||||
export type CollaborationPushMessage = CollaboratorsChanged;
|
9
packages/@n8n/api-types/src/push/debug.ts
Normal file
9
packages/@n8n/api-types/src/push/debug.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
type SendConsoleMessage = {
|
||||
type: 'sendConsoleMessage';
|
||||
data: {
|
||||
source: string;
|
||||
messages: unknown[];
|
||||
};
|
||||
};
|
||||
|
||||
export type DebugPushMessage = SendConsoleMessage;
|
53
packages/@n8n/api-types/src/push/execution.ts
Normal file
53
packages/@n8n/api-types/src/push/execution.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import type { IRun, ITaskData, WorkflowExecuteMode } from 'n8n-workflow';
|
||||
|
||||
type ExecutionStarted = {
|
||||
type: 'executionStarted';
|
||||
data: {
|
||||
executionId: string;
|
||||
mode: WorkflowExecuteMode;
|
||||
startedAt: Date;
|
||||
workflowId: string;
|
||||
workflowName?: string;
|
||||
retryOf?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type ExecutionFinished = {
|
||||
type: 'executionFinished';
|
||||
data: {
|
||||
executionId: string;
|
||||
data: IRun;
|
||||
retryOf?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type ExecutionRecovered = {
|
||||
type: 'executionRecovered';
|
||||
data: {
|
||||
executionId: string;
|
||||
};
|
||||
};
|
||||
|
||||
type NodeExecuteBefore = {
|
||||
type: 'nodeExecuteBefore';
|
||||
data: {
|
||||
executionId: string;
|
||||
nodeName: string;
|
||||
};
|
||||
};
|
||||
|
||||
type NodeExecuteAfter = {
|
||||
type: 'nodeExecuteAfter';
|
||||
data: {
|
||||
executionId: string;
|
||||
nodeName: string;
|
||||
data: ITaskData;
|
||||
};
|
||||
};
|
||||
|
||||
export type ExecutionPushMessage =
|
||||
| ExecutionStarted
|
||||
| ExecutionFinished
|
||||
| ExecutionRecovered
|
||||
| NodeExecuteBefore
|
||||
| NodeExecuteAfter;
|
21
packages/@n8n/api-types/src/push/hot-reload.ts
Normal file
21
packages/@n8n/api-types/src/push/hot-reload.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
type NodeTypeData = {
|
||||
name: string;
|
||||
version: number;
|
||||
};
|
||||
|
||||
type ReloadNodeType = {
|
||||
type: 'reloadNodeType';
|
||||
data: NodeTypeData;
|
||||
};
|
||||
|
||||
type RemoveNodeType = {
|
||||
type: 'removeNodeType';
|
||||
data: NodeTypeData;
|
||||
};
|
||||
|
||||
type NodeDescriptionUpdated = {
|
||||
type: 'nodeDescriptionUpdated';
|
||||
data: {};
|
||||
};
|
||||
|
||||
export type HotReloadPushMessage = ReloadNodeType | RemoveNodeType | NodeDescriptionUpdated;
|
20
packages/@n8n/api-types/src/push/index.ts
Normal file
20
packages/@n8n/api-types/src/push/index.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import type { ExecutionPushMessage } from './execution';
|
||||
import type { WorkflowPushMessage } from './workflow';
|
||||
import type { HotReloadPushMessage } from './hot-reload';
|
||||
import type { WorkerPushMessage } from './worker';
|
||||
import type { WebhookPushMessage } from './webhook';
|
||||
import type { CollaborationPushMessage } from './collaboration';
|
||||
import type { DebugPushMessage } from './debug';
|
||||
|
||||
export type PushMessage =
|
||||
| ExecutionPushMessage
|
||||
| WorkflowPushMessage
|
||||
| HotReloadPushMessage
|
||||
| WebhookPushMessage
|
||||
| WorkerPushMessage
|
||||
| CollaborationPushMessage
|
||||
| DebugPushMessage;
|
||||
|
||||
export type PushType = PushMessage['type'];
|
||||
|
||||
export type PushPayload<T extends PushType> = Extract<PushMessage, { type: T }>['data'];
|
17
packages/@n8n/api-types/src/push/webhook.ts
Normal file
17
packages/@n8n/api-types/src/push/webhook.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
type TestWebhookDeleted = {
|
||||
type: 'testWebhookDeleted';
|
||||
data: {
|
||||
executionId?: string;
|
||||
workflowId: string;
|
||||
};
|
||||
};
|
||||
|
||||
type TestWebhookReceived = {
|
||||
type: 'testWebhookReceived';
|
||||
data: {
|
||||
executionId: string;
|
||||
workflowId: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type WebhookPushMessage = TestWebhookDeleted | TestWebhookReceived;
|
11
packages/@n8n/api-types/src/push/worker.ts
Normal file
11
packages/@n8n/api-types/src/push/worker.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import type { WorkerStatus } from '../scaling';
|
||||
|
||||
export type SendWorkerStatusMessage = {
|
||||
type: 'sendWorkerStatusMessage';
|
||||
data: {
|
||||
workerId: string;
|
||||
status: WorkerStatus;
|
||||
};
|
||||
};
|
||||
|
||||
export type WorkerPushMessage = SendWorkerStatusMessage;
|
26
packages/@n8n/api-types/src/push/workflow.ts
Normal file
26
packages/@n8n/api-types/src/push/workflow.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
type WorkflowActivated = {
|
||||
type: 'workflowActivated';
|
||||
data: {
|
||||
workflowId: string;
|
||||
};
|
||||
};
|
||||
|
||||
type WorkflowFailedToActivate = {
|
||||
type: 'workflowFailedToActivate';
|
||||
data: {
|
||||
workflowId: string;
|
||||
errorMessage: string;
|
||||
};
|
||||
};
|
||||
|
||||
type WorkflowDeactivated = {
|
||||
type: 'workflowDeactivated';
|
||||
data: {
|
||||
workflowId: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type WorkflowPushMessage =
|
||||
| WorkflowActivated
|
||||
| WorkflowFailedToActivate
|
||||
| WorkflowDeactivated;
|
30
packages/@n8n/api-types/src/scaling.ts
Normal file
30
packages/@n8n/api-types/src/scaling.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import type { ExecutionStatus, WorkflowExecuteMode } from 'n8n-workflow';
|
||||
|
||||
export type RunningJobSummary = {
|
||||
executionId: string;
|
||||
workflowId: string;
|
||||
workflowName: string;
|
||||
mode: WorkflowExecuteMode;
|
||||
startedAt: Date;
|
||||
retryOf: string;
|
||||
status: ExecutionStatus;
|
||||
};
|
||||
|
||||
export type WorkerStatus = {
|
||||
workerId: string;
|
||||
runningJobsSummary: RunningJobSummary[];
|
||||
freeMem: number;
|
||||
totalMem: number;
|
||||
uptime: number;
|
||||
loadAvg: number[];
|
||||
cpus: string;
|
||||
arch: string;
|
||||
platform: NodeJS.Platform;
|
||||
hostname: string;
|
||||
interfaces: Array<{
|
||||
family: 'IPv4' | 'IPv6';
|
||||
address: string;
|
||||
internal: boolean;
|
||||
}>;
|
||||
version: string;
|
||||
};
|
6
packages/@n8n/api-types/src/user.ts
Normal file
6
packages/@n8n/api-types/src/user.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export type MinimalUser = {
|
||||
id: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
};
|
11
packages/@n8n/api-types/tsconfig.build.json
Normal file
11
packages/@n8n/api-types/tsconfig.build.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": ["./tsconfig.json", "../../../tsconfig.build.json"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"tsBuildInfoFile": "dist/build.tsbuildinfo"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["test/**", "src/**/__tests__/**"]
|
||||
}
|
10
packages/@n8n/api-types/tsconfig.json
Normal file
10
packages/@n8n/api-types/tsconfig.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"types": ["node", "jest"],
|
||||
"baseUrl": "src",
|
||||
"tsBuildInfoFile": "dist/typecheck.tsbuildinfo"
|
||||
},
|
||||
"include": ["src/**/*.ts", "test/**/*.ts"]
|
||||
}
|
31
packages/@n8n/benchmark/.eslintrc.js
Normal file
31
packages/@n8n/benchmark/.eslintrc.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
const sharedOptions = require('@n8n_io/eslint-config/shared');
|
||||
|
||||
/**
|
||||
* @type {import('@types/eslint').ESLint.ConfigData}
|
||||
*/
|
||||
module.exports = {
|
||||
extends: ['@n8n_io/eslint-config/node'],
|
||||
|
||||
...sharedOptions(__dirname),
|
||||
|
||||
parserOptions: {
|
||||
project: './tsconfig.json',
|
||||
},
|
||||
|
||||
ignorePatterns: ['scenarios/**'],
|
||||
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
'n8n-local-rules/no-plain-errors': 'off',
|
||||
complexity: 'error',
|
||||
},
|
||||
|
||||
overrides: [
|
||||
{
|
||||
files: ['./src/commands/*.ts'],
|
||||
rules: {
|
||||
'import/no-default-export': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
4
packages/@n8n/benchmark/.gitignore
vendored
Normal file
4
packages/@n8n/benchmark/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
**/.terraform/*
|
||||
**/*.tfstate*
|
||||
**/*.tfvars
|
||||
privatekey.pem
|
62
packages/@n8n/benchmark/Dockerfile
Normal file
62
packages/@n8n/benchmark/Dockerfile
Normal file
|
@ -0,0 +1,62 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
FROM node:20.16.0 AS base
|
||||
|
||||
# Install required dependencies
|
||||
RUN apt-get update && apt-get install -y gnupg2 curl
|
||||
|
||||
# Add k6 GPG key and repository
|
||||
RUN mkdir -p /etc/apt/keyrings && \
|
||||
curl -sS https://dl.k6.io/key.gpg | gpg --dearmor --yes -o /etc/apt/keyrings/k6.gpg && \
|
||||
chmod a+x /etc/apt/keyrings/k6.gpg && \
|
||||
echo "deb [signed-by=/etc/apt/keyrings/k6.gpg] https://dl.k6.io/deb stable main" | tee /etc/apt/sources.list.d/k6.list
|
||||
|
||||
# Update and install k6
|
||||
RUN apt-get update && \
|
||||
apt-get install -y k6 tini && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
|
||||
#
|
||||
# Builder
|
||||
FROM base AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --chown=node:node ./pnpm-lock.yaml /app/pnpm-lock.yaml
|
||||
COPY --chown=node:node ./pnpm-workspace.yaml /app/pnpm-workspace.yaml
|
||||
COPY --chown=node:node ./package.json /app/package.json
|
||||
COPY --chown=node:node ./packages/@n8n/benchmark/package.json /app/packages/@n8n/benchmark/package.json
|
||||
COPY --chown=node:node ./patches /app/patches
|
||||
COPY --chown=node:node ./scripts /app/scripts
|
||||
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# TS config files
|
||||
COPY --chown=node:node ./tsconfig.json /app/tsconfig.json
|
||||
COPY --chown=node:node ./tsconfig.build.json /app/tsconfig.build.json
|
||||
COPY --chown=node:node ./tsconfig.backend.json /app/tsconfig.backend.json
|
||||
COPY --chown=node:node ./packages/@n8n/benchmark/tsconfig.json /app/packages/@n8n/benchmark/tsconfig.json
|
||||
COPY --chown=node:node ./packages/@n8n/benchmark/tsconfig.build.json /app/packages/@n8n/benchmark/tsconfig.build.json
|
||||
|
||||
# Source files
|
||||
COPY --chown=node:node ./packages/@n8n/benchmark/src /app/packages/@n8n/benchmark/src
|
||||
COPY --chown=node:node ./packages/@n8n/benchmark/bin /app/packages/@n8n/benchmark/bin
|
||||
COPY --chown=node:node ./packages/@n8n/benchmark/scenarios /app/packages/@n8n/benchmark/scenarios
|
||||
|
||||
WORKDIR /app/packages/@n8n/benchmark
|
||||
RUN pnpm build
|
||||
|
||||
#
|
||||
# Runner
|
||||
FROM base AS runner
|
||||
|
||||
COPY --from=builder /app /app
|
||||
|
||||
WORKDIR /app/packages/@n8n/benchmark
|
||||
USER node
|
||||
|
||||
ENTRYPOINT [ "/app/packages/@n8n/benchmark/bin/n8n-benchmark" ]
|
82
packages/@n8n/benchmark/README.md
Normal file
82
packages/@n8n/benchmark/README.md
Normal file
|
@ -0,0 +1,82 @@
|
|||
# n8n benchmarking tool
|
||||
|
||||
Tool for executing benchmarks against an n8n instance. The tool consists of these components:
|
||||
|
||||
## Directory structure
|
||||
|
||||
```text
|
||||
packages/@n8n/benchmark
|
||||
├── scenarios Benchmark scenarios
|
||||
├── src Source code for the n8n-benchmark cli
|
||||
├── Dockerfile Dockerfile for the n8n-benchmark cli
|
||||
├── scripts Orchestration scripts
|
||||
```
|
||||
|
||||
## Running the entire benchmark suite
|
||||
|
||||
The benchmark suite consists of [benchmark scenarios](#benchmark-scenarios) and different [n8n setups](#n8n-setups).
|
||||
|
||||
### locally
|
||||
|
||||
```sh
|
||||
pnpm benchmark-locally
|
||||
```
|
||||
|
||||
### In the cloud
|
||||
|
||||
```sh
|
||||
pnpm benchmark-in-cloud
|
||||
```
|
||||
|
||||
## Running the `n8n-benchmark` cli
|
||||
|
||||
The `n8n-benchmark` cli is a node.js program that runs one or more scenarios against a single n8n instance.
|
||||
|
||||
### Locally with Docker
|
||||
|
||||
Build the Docker image:
|
||||
|
||||
```sh
|
||||
# Must be run in the repository root
|
||||
# k6 doesn't have an arm64 build available for linux, we need to build against amd64
|
||||
docker build --platform linux/amd64 -t n8n-benchmark -f packages/@n8n/benchmark/Dockerfile .
|
||||
```
|
||||
|
||||
Run the image
|
||||
|
||||
```sh
|
||||
docker run \
|
||||
-e N8N_USER_EMAIL=user@n8n.io \
|
||||
-e N8N_USER_PASSWORD=password \
|
||||
# For macos, n8n running outside docker
|
||||
-e N8N_BASE_URL=http://host.docker.internal:5678 \
|
||||
n8n-benchmark
|
||||
```
|
||||
|
||||
### Locally without Docker
|
||||
|
||||
Requirements:
|
||||
|
||||
- [k6](https://grafana.com/docs/k6/latest/set-up/install-k6/)
|
||||
- Node.js v20 or higher
|
||||
|
||||
```sh
|
||||
pnpm build
|
||||
|
||||
# Run tests against http://localhost:5678 with specified email and password
|
||||
N8N_USER_EMAIL=user@n8n.io N8N_USER_PASSWORD=password ./bin/n8n-benchmark run
|
||||
```
|
||||
|
||||
## Benchmark scenarios
|
||||
|
||||
A benchmark scenario defines one or multiple steps to execute and measure. It consists of:
|
||||
|
||||
- Manifest file which describes and configures the scenario
|
||||
- Any test data that is imported before the scenario is run
|
||||
- A [`k6`](https://grafana.com/docs/k6/latest/using-k6/http-requests/) script which executes the steps and receives `API_BASE_URL` environment variable in runtime.
|
||||
|
||||
Available scenarios are located in [`./scenarios`](./scenarios/).
|
||||
|
||||
## n8n setups
|
||||
|
||||
A n8n setup defines a single n8n runtime configuration using Docker compose. Different n8n setups are located in [`./scripts/n8nSetups`](./scripts/n8nSetups).
|
13
packages/@n8n/benchmark/bin/n8n-benchmark
Executable file
13
packages/@n8n/benchmark/bin/n8n-benchmark
Executable file
|
@ -0,0 +1,13 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// Check if version should be displayed
|
||||
const versionFlags = ['-v', '-V', '--version'];
|
||||
if (versionFlags.includes(process.argv.slice(-1)[0])) {
|
||||
console.log(require('../package').version);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const oclif = require('@oclif/core');
|
||||
await oclif.execute({ dir: __dirname });
|
||||
})();
|
60
packages/@n8n/benchmark/infra/.terraform.lock.hcl
Normal file
60
packages/@n8n/benchmark/infra/.terraform.lock.hcl
Normal file
|
@ -0,0 +1,60 @@
|
|||
# This file is maintained automatically by "terraform init".
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.terraform.io/hashicorp/azurerm" {
|
||||
version = "3.115.0"
|
||||
constraints = "~> 3.115.0"
|
||||
hashes = [
|
||||
"h1:O7C3Xb+MSOc9C/eAJ5C/CiJ4vuvUsYxxIzr9ZurmHNI=",
|
||||
"zh:0ea93abd53cb872691bad6d5625bda88b5d9619ea813c208b36e0ee236308589",
|
||||
"zh:26703cb9c2c38bc43e97bc83af03559d065750856ea85834b71fbcb2ef9d935c",
|
||||
"zh:316255a3391c49fe9bd7c5b6aa53b56dd490e1083d19b722e7b8f956a2dfe004",
|
||||
"zh:431637ae90c592126fb1ec813fee6390604275438a0d5e15904c65b0a6a0f826",
|
||||
"zh:4cee0fa2e84f89853723c0bc72b7debf8ea2ffffc7ae34ff28d8a69269d3a879",
|
||||
"zh:64a3a3c78ea877515365ed336bd0f3abbe71db7c99b3d2837915fbca168d429c",
|
||||
"zh:7380d7b503b5a87fd71a31360c3eeab504f78e4f314824e3ceda724d9dc74cf0",
|
||||
"zh:974213e05708037a6d2d8c58cc84981819138f44fe40e344034eb80e16ca6012",
|
||||
"zh:9a91614de0476074e9c62bbf08d3bb9c64adbd1d3a4a2b5a3e8e41d9d6d5672f",
|
||||
"zh:a438471c85b8788ab21bdef4cd5ca391a46cbae33bd0262668a80f5e6c4610e1",
|
||||
"zh:bf823f2c941b336a1208f015466212b1a8fdf6da28abacf59bea708377709d9e",
|
||||
"zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
|
||||
]
|
||||
}
|
||||
|
||||
provider "registry.terraform.io/hashicorp/random" {
|
||||
version = "3.6.2"
|
||||
hashes = [
|
||||
"h1:VavG5unYCa3SYISMKF9pzc3718M0bhPlcbUZZGl7wuo=",
|
||||
"zh:0ef01a4f81147b32c1bea3429974d4d104bbc4be2ba3cfa667031a8183ef88ec",
|
||||
"zh:1bcd2d8161e89e39886119965ef0f37fcce2da9c1aca34263dd3002ba05fcb53",
|
||||
"zh:37c75d15e9514556a5f4ed02e1548aaa95c0ecd6ff9af1119ac905144c70c114",
|
||||
"zh:4210550a767226976bc7e57d988b9ce48f4411fa8a60cd74a6b246baf7589dad",
|
||||
"zh:562007382520cd4baa7320f35e1370ffe84e46ed4e2071fdc7e4b1a9b1f8ae9b",
|
||||
"zh:5efb9da90f665e43f22c2e13e0ce48e86cae2d960aaf1abf721b497f32025916",
|
||||
"zh:6f71257a6b1218d02a573fc9bff0657410404fb2ef23bc66ae8cd968f98d5ff6",
|
||||
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
||||
"zh:9647e18f221380a85f2f0ab387c68fdafd58af6193a932417299cdcae4710150",
|
||||
"zh:bb6297ce412c3c2fa9fec726114e5e0508dd2638cad6a0cb433194930c97a544",
|
||||
"zh:f83e925ed73ff8a5ef6e3608ad9225baa5376446349572c2449c0c0b3cf184b7",
|
||||
"zh:fbef0781cb64de76b1df1ca11078aecba7800d82fd4a956302734999cfd9a4af",
|
||||
]
|
||||
}
|
||||
|
||||
provider "registry.terraform.io/hashicorp/tls" {
|
||||
version = "4.0.5"
|
||||
hashes = [
|
||||
"h1:zeG5RmggBZW/8JWIVrdaeSJa0OG62uFX5HY1eE8SjzY=",
|
||||
"zh:01cfb11cb74654c003f6d4e32bbef8f5969ee2856394a96d127da4949c65153e",
|
||||
"zh:0472ea1574026aa1e8ca82bb6df2c40cd0478e9336b7a8a64e652119a2fa4f32",
|
||||
"zh:1a8ddba2b1550c5d02003ea5d6cdda2eef6870ece86c5619f33edd699c9dc14b",
|
||||
"zh:1e3bb505c000adb12cdf60af5b08f0ed68bc3955b0d4d4a126db5ca4d429eb4a",
|
||||
"zh:6636401b2463c25e03e68a6b786acf91a311c78444b1dc4f97c539f9f78de22a",
|
||||
"zh:76858f9d8b460e7b2a338c477671d07286b0d287fd2d2e3214030ae8f61dd56e",
|
||||
"zh:a13b69fb43cb8746793b3069c4d897bb18f454290b496f19d03c3387d1c9a2dc",
|
||||
"zh:a90ca81bb9bb509063b736842250ecff0f886a91baae8de65c8430168001dad9",
|
||||
"zh:c4de401395936e41234f1956ebadbd2ed9f414e6908f27d578614aaa529870d4",
|
||||
"zh:c657e121af8fde19964482997f0de2d5173217274f6997e16389e7707ed8ece8",
|
||||
"zh:d68b07a67fbd604c38ec9733069fbf23441436fecf554de6c75c032f82e1ef19",
|
||||
"zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
|
||||
]
|
||||
}
|
54
packages/@n8n/benchmark/infra/benchmark-env.tf
Normal file
54
packages/@n8n/benchmark/infra/benchmark-env.tf
Normal file
|
@ -0,0 +1,54 @@
|
|||
|
||||
data "azurerm_resource_group" "main" {
|
||||
name = var.resource_group_name
|
||||
}
|
||||
|
||||
# Random prefix for the resources
|
||||
resource "random_string" "prefix" {
|
||||
length = 8
|
||||
special = false
|
||||
}
|
||||
|
||||
# SSH key pair
|
||||
resource "tls_private_key" "ssh_key" {
|
||||
algorithm = "RSA"
|
||||
rsa_bits = 4096
|
||||
}
|
||||
|
||||
# Dedicated Host Group & Hosts
|
||||
|
||||
resource "azurerm_dedicated_host_group" "main" {
|
||||
name = "${random_string.prefix.result}-hostgroup"
|
||||
location = var.location
|
||||
resource_group_name = data.azurerm_resource_group.main.name
|
||||
platform_fault_domain_count = 1
|
||||
automatic_placement_enabled = false
|
||||
zone = 1
|
||||
|
||||
tags = local.common_tags
|
||||
}
|
||||
|
||||
resource "azurerm_dedicated_host" "hosts" {
|
||||
name = "${random_string.prefix.result}-host"
|
||||
location = var.location
|
||||
dedicated_host_group_id = azurerm_dedicated_host_group.main.id
|
||||
sku_name = var.host_size_family
|
||||
platform_fault_domain = 0
|
||||
|
||||
tags = local.common_tags
|
||||
}
|
||||
|
||||
# VM
|
||||
|
||||
module "test_vm" {
|
||||
source = "./modules/benchmark-vm"
|
||||
|
||||
location = var.location
|
||||
resource_group_name = data.azurerm_resource_group.main.name
|
||||
prefix = random_string.prefix.result
|
||||
dedicated_host_id = azurerm_dedicated_host.hosts.id
|
||||
ssh_public_key = tls_private_key.ssh_key.public_key_openssh
|
||||
vm_size = var.vm_size
|
||||
|
||||
tags = local.common_tags
|
||||
}
|
11
packages/@n8n/benchmark/infra/modules/benchmark-vm/output.tf
Normal file
11
packages/@n8n/benchmark/infra/modules/benchmark-vm/output.tf
Normal file
|
@ -0,0 +1,11 @@
|
|||
output "vm_name" {
|
||||
value = azurerm_linux_virtual_machine.main.name
|
||||
}
|
||||
|
||||
output "ip" {
|
||||
value = azurerm_public_ip.main.ip_address
|
||||
}
|
||||
|
||||
output "ssh_username" {
|
||||
value = azurerm_linux_virtual_machine.main.admin_username
|
||||
}
|
29
packages/@n8n/benchmark/infra/modules/benchmark-vm/vars.tf
Normal file
29
packages/@n8n/benchmark/infra/modules/benchmark-vm/vars.tf
Normal file
|
@ -0,0 +1,29 @@
|
|||
variable "location" {
|
||||
description = "Region to deploy resources"
|
||||
default = "East US"
|
||||
}
|
||||
|
||||
variable "resource_group_name" {
|
||||
description = "Name of the resource group"
|
||||
}
|
||||
|
||||
variable "prefix" {
|
||||
description = "Prefix to append to resources"
|
||||
}
|
||||
|
||||
variable "dedicated_host_id" {
|
||||
description = "Dedicated Host ID"
|
||||
}
|
||||
|
||||
variable "ssh_public_key" {
|
||||
description = "SSH Public Key"
|
||||
}
|
||||
|
||||
variable "vm_size" {
|
||||
description = "VM Size"
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
description = "Tags to apply to all resources created by this module"
|
||||
type = map(string)
|
||||
}
|
126
packages/@n8n/benchmark/infra/modules/benchmark-vm/vm.tf
Normal file
126
packages/@n8n/benchmark/infra/modules/benchmark-vm/vm.tf
Normal file
|
@ -0,0 +1,126 @@
|
|||
# Network
|
||||
|
||||
resource "azurerm_virtual_network" "main" {
|
||||
name = "${var.prefix}-vnet"
|
||||
location = var.location
|
||||
resource_group_name = var.resource_group_name
|
||||
address_space = ["10.0.0.0/16"]
|
||||
|
||||
tags = var.tags
|
||||
}
|
||||
|
||||
resource "azurerm_subnet" "main" {
|
||||
name = "${var.prefix}-subnet"
|
||||
resource_group_name = var.resource_group_name
|
||||
virtual_network_name = azurerm_virtual_network.main.name
|
||||
address_prefixes = ["10.0.0.0/24"]
|
||||
}
|
||||
|
||||
resource "azurerm_network_security_group" "ssh" {
|
||||
name = "${var.prefix}-nsg"
|
||||
location = var.location
|
||||
resource_group_name = var.resource_group_name
|
||||
|
||||
security_rule {
|
||||
name = "AllowSSH"
|
||||
priority = 1001
|
||||
direction = "Inbound"
|
||||
access = "Allow"
|
||||
protocol = "Tcp"
|
||||
source_port_range = "*"
|
||||
destination_port_range = "22"
|
||||
source_address_prefix = "*"
|
||||
destination_address_prefix = "*"
|
||||
}
|
||||
|
||||
tags = var.tags
|
||||
}
|
||||
|
||||
resource "azurerm_public_ip" "main" {
|
||||
name = "${var.prefix}-pip"
|
||||
location = var.location
|
||||
resource_group_name = var.resource_group_name
|
||||
allocation_method = "Static"
|
||||
sku = "Standard"
|
||||
|
||||
tags = var.tags
|
||||
}
|
||||
|
||||
resource "azurerm_network_interface" "main" {
|
||||
name = "${var.prefix}-nic"
|
||||
location = var.location
|
||||
resource_group_name = var.resource_group_name
|
||||
|
||||
ip_configuration {
|
||||
name = "${var.prefix}-ipconfig"
|
||||
subnet_id = azurerm_subnet.main.id
|
||||
private_ip_address_allocation = "Dynamic"
|
||||
public_ip_address_id = azurerm_public_ip.main.id
|
||||
}
|
||||
|
||||
tags = var.tags
|
||||
}
|
||||
|
||||
resource "azurerm_network_interface_security_group_association" "ssh" {
|
||||
network_interface_id = azurerm_network_interface.main.id
|
||||
network_security_group_id = azurerm_network_security_group.ssh.id
|
||||
}
|
||||
|
||||
# Disk
|
||||
|
||||
resource "azurerm_managed_disk" "data" {
|
||||
name = "${var.prefix}-disk"
|
||||
location = var.location
|
||||
resource_group_name = var.resource_group_name
|
||||
storage_account_type = "PremiumV2_LRS"
|
||||
create_option = "Empty"
|
||||
disk_size_gb = "16"
|
||||
zone = 1
|
||||
|
||||
tags = var.tags
|
||||
}
|
||||
|
||||
resource "azurerm_virtual_machine_data_disk_attachment" "data" {
|
||||
managed_disk_id = azurerm_managed_disk.data.id
|
||||
virtual_machine_id = azurerm_linux_virtual_machine.main.id
|
||||
lun = "1"
|
||||
caching = "None"
|
||||
}
|
||||
|
||||
# VM
|
||||
|
||||
resource "azurerm_linux_virtual_machine" "main" {
|
||||
name = "${var.prefix}-vm"
|
||||
location = var.location
|
||||
resource_group_name = var.resource_group_name
|
||||
network_interface_ids = [azurerm_network_interface.main.id]
|
||||
dedicated_host_id = var.dedicated_host_id
|
||||
zone = 1
|
||||
|
||||
size = var.vm_size
|
||||
|
||||
admin_username = "benchmark"
|
||||
|
||||
admin_ssh_key {
|
||||
username = "benchmark"
|
||||
public_key = var.ssh_public_key
|
||||
}
|
||||
|
||||
os_disk {
|
||||
caching = "ReadWrite"
|
||||
storage_account_type = "Premium_LRS"
|
||||
}
|
||||
|
||||
source_image_reference {
|
||||
publisher = "Canonical"
|
||||
offer = "0001-com-ubuntu-server-jammy"
|
||||
sku = "22_04-lts-gen2"
|
||||
version = "latest"
|
||||
}
|
||||
|
||||
identity {
|
||||
type = "SystemAssigned"
|
||||
}
|
||||
|
||||
tags = var.tags
|
||||
}
|
16
packages/@n8n/benchmark/infra/output.tf
Normal file
16
packages/@n8n/benchmark/infra/output.tf
Normal file
|
@ -0,0 +1,16 @@
|
|||
output "vm_name" {
|
||||
value = module.test_vm.vm_name
|
||||
}
|
||||
|
||||
output "ip" {
|
||||
value = module.test_vm.ip
|
||||
}
|
||||
|
||||
output "ssh_username" {
|
||||
value = module.test_vm.ssh_username
|
||||
}
|
||||
|
||||
output "ssh_private_key" {
|
||||
value = tls_private_key.ssh_key.private_key_pem
|
||||
sensitive = true
|
||||
}
|
23
packages/@n8n/benchmark/infra/providers.tf
Normal file
23
packages/@n8n/benchmark/infra/providers.tf
Normal file
|
@ -0,0 +1,23 @@
|
|||
|
||||
terraform {
|
||||
required_providers {
|
||||
azurerm = {
|
||||
source = "hashicorp/azurerm"
|
||||
version = "~> 3.115.0"
|
||||
}
|
||||
|
||||
random = {
|
||||
source = "hashicorp/random"
|
||||
}
|
||||
}
|
||||
|
||||
required_version = "~> 1.8.5"
|
||||
}
|
||||
|
||||
provider "azurerm" {
|
||||
features {}
|
||||
|
||||
skip_provider_registration = true
|
||||
}
|
||||
|
||||
provider "random" {}
|
34
packages/@n8n/benchmark/infra/vars.tf
Normal file
34
packages/@n8n/benchmark/infra/vars.tf
Normal file
|
@ -0,0 +1,34 @@
|
|||
variable "location" {
|
||||
description = "Region to deploy resources"
|
||||
default = "East US"
|
||||
}
|
||||
|
||||
variable "resource_group_name" {
|
||||
description = "Name of the resource group"
|
||||
default = "n8n-benchmarking"
|
||||
}
|
||||
|
||||
variable "host_size_family" {
|
||||
description = "Size Family for the Host Group"
|
||||
default = "DCSv2-Type1"
|
||||
}
|
||||
|
||||
variable "vm_size" {
|
||||
description = "VM Size"
|
||||
# 8 vCPUs, 32 GiB memory
|
||||
default = "Standard_DC8_v2"
|
||||
}
|
||||
|
||||
variable "number_of_vms" {
|
||||
description = "Number of VMs to create"
|
||||
default = 1
|
||||
}
|
||||
|
||||
locals {
|
||||
common_tags = {
|
||||
Id = "N8nBenchmark"
|
||||
Terraform = "true"
|
||||
Owner = "Catalysts"
|
||||
CreatedAt = timestamp()
|
||||
}
|
||||
}
|
55
packages/@n8n/benchmark/package.json
Normal file
55
packages/@n8n/benchmark/package.json
Normal file
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"name": "@n8n/n8n-benchmark",
|
||||
"version": "1.3.0",
|
||||
"description": "Cli for running benchmark tests for n8n",
|
||||
"main": "dist/index",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
|
||||
"lint": "eslint .",
|
||||
"lintfix": "eslint . --fix",
|
||||
"start": "./bin/n8n-benchmark",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"benchmark": "zx scripts/run.mjs",
|
||||
"benchmark-in-cloud": "pnpm benchmark --env cloud",
|
||||
"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\""
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.10"
|
||||
},
|
||||
"keywords": [
|
||||
"automate",
|
||||
"automation",
|
||||
"IaaS",
|
||||
"iPaaS",
|
||||
"n8n",
|
||||
"workflow",
|
||||
"benchmark",
|
||||
"performance"
|
||||
],
|
||||
"dependencies": {
|
||||
"@oclif/core": "4.0.7",
|
||||
"axios": "catalog:",
|
||||
"dotenv": "8.6.0",
|
||||
"nanoid": "catalog:",
|
||||
"zx": "^8.1.4"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"bin": {
|
||||
"n8n-benchmark": "./bin/n8n-benchmark"
|
||||
},
|
||||
"oclif": {
|
||||
"bin": "n8n-benchmark",
|
||||
"commands": "./dist/commands",
|
||||
"topicSeparator": " "
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
{
|
||||
"createdAt": "2024-09-03T11:51:56.540Z",
|
||||
"updatedAt": "2024-09-03T12:22:21.000Z",
|
||||
"name": "Binary Data",
|
||||
"active": true,
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "binary-files-benchmark",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"position": [0, 0],
|
||||
"id": "bfe19f12-3655-440f-be5c-8d71665c6353",
|
||||
"name": "Webhook",
|
||||
"webhookId": "109d7b13-93ad-42b0-a9ce-ca49e1817b35"
|
||||
},
|
||||
{
|
||||
"parameters": { "respondWith": "binary", "options": {} },
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"position": [740, 0],
|
||||
"id": "cd957c9b-6b7a-4423-aac3-6df4d8bb571e",
|
||||
"name": "Respond to Webhook"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "write",
|
||||
"fileName": "=file-{{ Date.now() }}-{{ Math.random() }}.js",
|
||||
"dataPropertyName": "file",
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.readWriteFile",
|
||||
"typeVersion": 1,
|
||||
"position": [260, 0],
|
||||
"id": "f2ce4709-7697-4bc6-8eca-6c222485297a",
|
||||
"name": "Write File to Disk"
|
||||
},
|
||||
{
|
||||
"parameters": { "fileSelector": "={{ $json.fileName }}", "options": {} },
|
||||
"type": "n8n-nodes-base.readWriteFile",
|
||||
"typeVersion": 1,
|
||||
"position": [500, 0],
|
||||
"id": "198e8a6c-81a3-4b34-b099-501961a02006",
|
||||
"name": "Read File from Disk"
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Webhook": { "main": [[{ "node": "Write File to Disk", "type": "main", "index": 0 }]] },
|
||||
"Write File to Disk": {
|
||||
"main": [[{ "node": "Read File from Disk", "type": "main", "index": 0 }]]
|
||||
},
|
||||
"Read File from Disk": {
|
||||
"main": [[{ "node": "Respond to Webhook", "type": "main", "index": 0 }]]
|
||||
}
|
||||
},
|
||||
"settings": { "executionOrder": "v1" },
|
||||
"staticData": null,
|
||||
"meta": null,
|
||||
"pinData": {},
|
||||
"versionId": "8dd197c0-d1ea-43c3-9f88-9d11e7b081a0",
|
||||
"triggerCount": 1,
|
||||
"tags": []
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"$schema": "../scenario.schema.json",
|
||||
"name": "BinaryData",
|
||||
"description": "Send a binary file to a webhook, write it to FS, read it from FS and receive it back",
|
||||
"scenarioData": { "workflowFiles": ["binary-data.json"] },
|
||||
"scriptPath": "binary-data.script.js"
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import http from 'k6/http';
|
||||
import { check } from 'k6';
|
||||
|
||||
const apiBaseUrl = __ENV.API_BASE_URL;
|
||||
|
||||
const file = open(__ENV.SCRIPT_FILE_PATH, 'b');
|
||||
const filename = String(__ENV.SCRIPT_FILE_PATH).split('/').pop();
|
||||
|
||||
export default function () {
|
||||
const data = {
|
||||
filename,
|
||||
file: http.file(file, filename, 'application/javascript'),
|
||||
};
|
||||
|
||||
const res = http.post(`${apiBaseUrl}/webhook/binary-files-benchmark`, data);
|
||||
|
||||
check(res, {
|
||||
'is status 200': (r) => r.status === 200,
|
||||
'has correct content type': (r) =>
|
||||
r.headers['Content-Type'] === 'application/javascript; charset=utf-8',
|
||||
});
|
||||
}
|
213
packages/@n8n/benchmark/scenarios/http-node/http-node.json
Normal file
213
packages/@n8n/benchmark/scenarios/http-node/http-node.json
Normal file
|
@ -0,0 +1,213 @@
|
|||
{
|
||||
"createdAt": "2024-09-04T07:18:29.011Z",
|
||||
"updatedAt": "2024-09-04T07:27:58.000Z",
|
||||
"id": "rUXzWNGsUDUmgaFS",
|
||||
"name": "HTTP Request",
|
||||
"active": false,
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "benchmark-http-node",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"position": [-60, 20],
|
||||
"id": "f11378b4-5f28-4a6c-9332-5878342cd3cf",
|
||||
"name": "Webhook",
|
||||
"webhookId": "c40014cc-4d64-4fcf-8c13-9e94b6792756"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"respondWith": "allIncomingItems",
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"position": [1060, 20],
|
||||
"id": "f42552c7-9c6e-4616-b9d5-ac79445ef4ed",
|
||||
"name": "Respond to Webhook"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "http://mockapi:8080/users/clair.bahringer/received_events/public",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"fullResponse": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [300, -180],
|
||||
"id": "20de816e-0fbe-4e28-bc53-f508a2dda117",
|
||||
"name": "Mock public received events"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "http://mockapi:8080/repos/udke6pujoywnagxkcvab2riw23khzn2tibo2vincws32qexb50ey7h97d42vnzyol0rxypgsg4pomsf7sgnmdaihstljw8edcijrwmy7mfi76yif19c4/47i31dh737el215j62ts2f2782nw3ss26rul3s8jw13u3vu0xm349a5hyay5asmwnlnf7nx8p9h4g62so6s1cis7xv9puj5j98t4m980sbe2455fn1obccjl/events",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"fullResponse": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [300, 20],
|
||||
"id": "083e02b3-a257-49a8-8f7d-42222cb9194c",
|
||||
"name": "Mock repository events"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "http://mockapi:8080/orgs/g02pp066qoyithcjevhd6m1wfii3c4x51k39n9apybljhx69/events",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"fullResponse": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [300, 220],
|
||||
"id": "f4c3b5d2-0257-4883-a585-ade4c3a1082c",
|
||||
"name": "Mock organization events"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"numberInputs": 3
|
||||
},
|
||||
"type": "n8n-nodes-base.merge",
|
||||
"typeVersion": 3,
|
||||
"position": [600, 20],
|
||||
"id": "273985b7-b0ae-4cde-bbe9-7b3e4b29fe61",
|
||||
"name": "Merge"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "89608adb-f487-416f-a7d8-3ebb1f7b50e5",
|
||||
"name": "statusCode",
|
||||
"value": "={{ $json.statusCode }}",
|
||||
"type": "number"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "231275a7-44e7-47bb-8ccf-fe62dc48356b",
|
||||
"name": "Select statusCode",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [820, 20]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Mock public received events",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Mock repository events",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Mock organization events",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Mock public received events": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Merge",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Mock repository events": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Merge",
|
||||
"type": "main",
|
||||
"index": 1
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Mock organization events": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Merge",
|
||||
"type": "main",
|
||||
"index": 2
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Merge": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Select statusCode",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Select statusCode": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Respond to Webhook",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": { "executionOrder": "v1" },
|
||||
"staticData": null,
|
||||
"meta": null,
|
||||
"pinData": {
|
||||
"Webhook": [
|
||||
{
|
||||
"json": {
|
||||
"headers": { "host": "localhost:5678", "user-agent": "curl/8.6.0", "accept": "*/*" },
|
||||
"params": {},
|
||||
"query": {},
|
||||
"body": {},
|
||||
"webhookUrl": "http://localhost:5678/webhook-test/benchmark-http-node",
|
||||
"executionMode": "test"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"versionId": "9fa91e54-e73a-4a34-b781-d64f2b02f333",
|
||||
"triggerCount": 0,
|
||||
"tags": []
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue