Merge branch 'fix/appbuilder-03' into gh-12680-toggle-app-mode

This commit is contained in:
Nakul Nagargade 2025-07-10 19:09:59 +05:30
commit d02b3d8c5c
620 changed files with 17433 additions and 7134 deletions

View file

@ -93,6 +93,10 @@ ENABLE_PRIVATE_APP_EMBED=
#Enable cors else restricted to TOOLJET_HOST. Set the value true if you are serving front end from diffrent host
ENABLE_CORS=
# cloud specific variables
ORGANIZATION_LICENSE_URL=
ORGANIZATION_LICENSE_API_KEY=
#pat session expiry in minutes
PAT_SESSION_EXPIRY=

133
.github/workflows/cloud-frontend-gcp.yml vendored Normal file
View file

@ -0,0 +1,133 @@
name: Deploy to cloud frontend stage
on:
workflow_dispatch:
inputs:
branch:
description: 'Git branch to deploy (must start with "lts-", e.g., lts-3.6)'
required: true
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: ✅ Check user authorization
run: |
allowed_user1=${{ secrets.ALLOWED_USER1_USERNAME }}
allowed_user2=${{ secrets.ALLOWED_USER2_USERNAME }}
allowed_user3=${{ secrets.ALLOWED_USER3_USERNAME }}
if [[ "${{ github.actor }}" != "$allowed_user1" && \
"${{ github.actor }}" != "$allowed_user2" && \
"${{ github.actor }}" != "$allowed_user3" ]]; then
echo "❌ User '${{ github.actor }}' is not authorized to trigger this workflow."
exit 1
else
echo "✅ User '${{ github.actor }}' is authorized."
fi
- name: 📥 Manual Git checkout with submodules
run: |
set -e
BRANCH="${{ github.event.inputs.branch }}"
REPO="https://x-access-token:${{ secrets.CUSTOM_GITHUB_TOKEN }}@github.com/${{ github.repository }}"
git config --global url."https://x-access-token:${{ secrets.CUSTOM_GITHUB_TOKEN }}@github.com/".insteadOf "https://github.com/"
git config --global http.version HTTP/1.1
git config --global http.postBuffer 524288000
echo "👉 Cloning $REPO (branch: $BRANCH)"
git clone --recurse-submodules --depth=1 --branch "$BRANCH" "$REPO" repo
cd repo
echo "🔁 Updating submodules"
git submodule update --init --recursive
echo "🔀 Attempting to checkout '$BRANCH' in each submodule and validating"
BRANCH="$BRANCH" git submodule foreach --recursive bash -c '
name="$sm_path"
echo ""
echo "Entering '\''$name'\''"
echo "↪ $name: trying to checkout branch '\''$BRANCH'\''"
if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null; then
git fetch origin "$BRANCH:$BRANCH" || {
echo "❌ $name: fetch failed for $BRANCH"
exit 1
}
PREV=$(git rev-parse --short HEAD || echo "unknown")
git checkout "$BRANCH" || {
echo "❌ $name: checkout failed for $BRANCH"
exit 1
}
echo "Previous HEAD position was $PREV: $(git log -1 --pretty=%s || echo 'unknown')"
echo "✅ $name: checked out branch $BRANCH"
else
echo "⚠️ $name: branch '$BRANCH' not found on origin. Falling back to 'main'"
PREV=$(git rev-parse --short HEAD || echo "unknown")
git checkout main && git pull origin main || {
echo "❌ $name: fallback to main failed"
exit 1
}
echo "Previous HEAD position was $PREV: $(git log -1 --pretty=%s || echo 'unknown')"
echo "✅ $name: now on branch main"
fi
CURRENT=$(git rev-parse --abbrev-ref HEAD)
echo "🔎 $name: current branch = $CURRENT"
if [ "$CURRENT" != "$BRANCH" ] && [ "$CURRENT" != "main" ]; then
echo "❌ $name: unexpected branch state — wanted '$BRANCH' or fallback 'main', got '$CURRENT'"
exit 1
fi
'
- name: 🧰 Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 22.15.1
- name: 📦 Install dependencies
run: npm install
working-directory: repo
- name: 🛠️ Build the project
run: npm run build:plugins:prod && npm run build:frontend
working-directory: repo
env:
GOOGLE_MAPS_API_KEY: ${{ secrets.CLOUD_GOOGLE_MAPS_API_KEY }}
NODE_ENV: ${{ secrets.CLOUD_NODE_ENV }}
NODE_OPTIONS: ${{ secrets.CLOUD_NODE_OPTIONS }}
SENTRY_AUTH_TOKEN: ${{ secrets.CLOUD_SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.CLOUD_SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.CLOUD_SENTRY_PROJECT }}
SERVE_CLIENT: ${{ secrets.CLOUD_SERVE_CLIENT }}
SERVER_IP: ${{ secrets.CLOUD_SERVER_IP }}
TJDB_SQL_MODE_DISABLE: ${{ secrets.CLOUD_TJDB_SQL_MODE_DISABLE }}
TOOLJET_SERVER_URL: ${{ secrets.CLOUD_TOOLJET_SERVER_URL }}
TOOLJET_EDITION: cloud
WEBSITE_SIGNUP_URL: https://website-stage.tooljet.ai/ai-create-account
- name: 🚀 Deploy to Netlify
run: |
npm install -g netlify-cli
netlify deploy --prod --dir=frontend/build --auth=$NETLIFY_AUTH_TOKEN --site=${{ secrets.CLOUD_NETLIFY_SITE_ID }}
working-directory: repo
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
GOOGLE_MAPS_API_KEY: ${{ secrets.CLOUD_GOOGLE_MAPS_API_KEY }}
NODE_ENV: ${{ secrets.CLOUD_NODE_ENV }}
NODE_OPTIONS: ${{ secrets.CLOUD_NODE_OPTIONS }}
SENTRY_AUTH_TOKEN: ${{ secrets.CLOUD_SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.CLOUD_SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.CLOUD_SENTRY_PROJECT }}
SERVE_CLIENT: ${{ secrets.CLOUD_SERVE_CLIENT }}
SERVER_IP: ${{ secrets.CLOUD_SERVER_IP }}
TJDB_SQL_MODE_DISABLE: ${{ secrets.CLOUD_TJDB_SQL_MODE_DISABLE }}
TOOLJET_SERVER_URL: ${{ secrets.CLOUD_TOOLJET_SERVER_URL }}
WEBSITE_SIGNUP_URL: https://website-stage.tooljet.ai/ai-create-account
TOOLJET_EDITION: cloud

133
.github/workflows/cloud-frontend.yml vendored Normal file
View file

@ -0,0 +1,133 @@
name: Deploy to cloud frontend
on:
workflow_dispatch:
inputs:
branch:
description: 'Git branch to deploy (must start with "lts-", e.g., lts-3.6)'
required: true
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: ✅ Check user authorization
run: |
allowed_user1=${{ secrets.ALLOWED_USER1_USERNAME }}
allowed_user2=${{ secrets.ALLOWED_USER2_USERNAME }}
allowed_user3=${{ secrets.ALLOWED_USER3_USERNAME }}
if [[ "${{ github.actor }}" != "$allowed_user1" && \
"${{ github.actor }}" != "$allowed_user2" && \
"${{ github.actor }}" != "$allowed_user3" ]]; then
echo "❌ User '${{ github.actor }}' is not authorized to trigger this workflow."
exit 1
else
echo "✅ User '${{ github.actor }}' is authorized."
fi
- name: 📥 Manual Git checkout with submodules
run: |
set -e
BRANCH="${{ github.event.inputs.branch }}"
REPO="https://x-access-token:${{ secrets.CUSTOM_GITHUB_TOKEN }}@github.com/${{ github.repository }}"
git config --global url."https://x-access-token:${{ secrets.CUSTOM_GITHUB_TOKEN }}@github.com/".insteadOf "https://github.com/"
git config --global http.version HTTP/1.1
git config --global http.postBuffer 524288000
echo "👉 Cloning $REPO (branch: $BRANCH)"
git clone --recurse-submodules --depth=1 --branch "$BRANCH" "$REPO" repo
cd repo
echo "🔁 Updating submodules"
git submodule update --init --recursive
echo "🔀 Attempting to checkout '$BRANCH' in each submodule and validating"
BRANCH="$BRANCH" git submodule foreach --recursive bash -c '
name="$sm_path"
echo ""
echo "Entering '\''$name'\''"
echo "↪ $name: trying to checkout branch '\''$BRANCH'\''"
if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null; then
git fetch origin "$BRANCH:$BRANCH" || {
echo "❌ $name: fetch failed for $BRANCH"
exit 1
}
PREV=$(git rev-parse --short HEAD || echo "unknown")
git checkout "$BRANCH" || {
echo "❌ $name: checkout failed for $BRANCH"
exit 1
}
echo "Previous HEAD position was $PREV: $(git log -1 --pretty=%s || echo 'unknown')"
echo "✅ $name: checked out branch $BRANCH"
else
echo "⚠️ $name: branch '$BRANCH' not found on origin. Falling back to 'main'"
PREV=$(git rev-parse --short HEAD || echo "unknown")
git checkout main && git pull origin main || {
echo "❌ $name: fallback to main failed"
exit 1
}
echo "Previous HEAD position was $PREV: $(git log -1 --pretty=%s || echo 'unknown')"
echo "✅ $name: now on branch main"
fi
CURRENT=$(git rev-parse --abbrev-ref HEAD)
echo "🔎 $name: current branch = $CURRENT"
if [ "$CURRENT" != "$BRANCH" ] && [ "$CURRENT" != "main" ]; then
echo "❌ $name: unexpected branch state — wanted '$BRANCH' or fallback 'main', got '$CURRENT'"
exit 1
fi
'
- name: 🧰 Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 22.15.1
- name: 📦 Install dependencies
run: npm install
working-directory: repo
- name: 🛠️ Build the project
run: npm run build:plugins:prod && npm run build:frontend
working-directory: repo
env:
GOOGLE_MAPS_API_KEY: ${{ secrets.CLOUD_PROD_CLOUD_GOOGLE_MAPS_API_KEY }}
NODE_ENV: ${{ secrets.CLOUD_NODE_ENV }}
NODE_OPTIONS: ${{ secrets.CLOUD_NODE_OPTIONS }}
SENTRY_AUTH_TOKEN: ${{ secrets.CLOUD_PROD_CLOUD_SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.CLOUD_PROD_CLOUD_SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.CLOUD_PROD_CLOUD_SENTRY_PROJECT }}
SERVE_CLIENT: ${{ secrets.CLOUD_PROD_CLOUD_SERVE_CLIENT }}
SERVER_IP: ${{ secrets.CLOUD_PROD_CLOUD_SERVER_IP }}
TJDB_SQL_MODE_DISABLE: ${{ secrets.CLOUD_TJDB_SQL_MODE_DISABLE }}
TOOLJET_SERVER_URL: ${{ secrets.CLOUD_TOOLJET_SERVER_URL }}
WEBSITE_SIGNUP_URL: https://tooljet.ai/ai-create-account
TOOLJET_EDITION: cloud
- name: 🚀 Deploy to Netlify
run: |
npm install -g netlify-cli
netlify deploy --prod --dir=frontend/build --auth=$NETLIFY_AUTH_TOKEN --site=${{ secrets.CLOUD_PROD_NETLIFY_SITE_ID }}
working-directory: repo
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
GOOGLE_MAPS_API_KEY: ${{ secrets.CLOUD_PROD_CLOUD_GOOGLE_MAPS_API_KEY }}
NODE_ENV: ${{ secrets.CLOUD_NODE_ENV }}
NODE_OPTIONS: ${{ secrets.CLOUD_NODE_OPTIONS }}
SENTRY_AUTH_TOKEN: ${{ secrets.CLOUD_PROD_CLOUD_SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.CLOUD_PROD_CLOUD_SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.CLOUD_PROD_CLOUD_SENTRY_PROJECT }}
SERVE_CLIENT: ${{ secrets.CLOUD_PROD_CLOUD_SERVE_CLIENT }}
SERVER_IP: ${{ secrets.CLOUD_PROD_CLOUD_SERVER_IP }}
TJDB_SQL_MODE_DISABLE: ${{ secrets.CLOUD_TJDB_SQL_MODE_DISABLE }}
TOOLJET_SERVER_URL: ${{ secrets.CLOUD_TOOLJET_SERVER_URL }}
WEBSITE_SIGNUP_URL: https://tooljet.ai/ai-create-account
TOOLJET_EDITION: cloud

View file

@ -21,7 +21,7 @@ jobs:
if: "contains(github.event.release.tag_name, '-ce-lts')"
uses: actions/checkout@v2
with:
ref: refs/heads/lts-4.0
ref: refs/heads/lts-3.6
# Create Docker Buildx builder with platform configuration
- name: Set up Docker Buildx
@ -99,7 +99,7 @@ jobs:
steps:
- name: Checkout code to main for pre-release EE edition
if: "!contains(github.event.release.tag_name, 'ee-lts')"
if: "!contains(github.event.release.tag_name, '-lts')"
uses: actions/checkout@v2
with:
ref: refs/heads/main
@ -108,7 +108,7 @@ jobs:
if: "contains(github.event.release.tag_name, '-ee-lts')"
uses: actions/checkout@v2
with:
ref: refs/heads/lts-4.0
ref: refs/heads/lts-3.6
# Create Docker Buildx builder with platform configuration
- name: Set up Docker Buildx
@ -139,15 +139,15 @@ jobs:
context: .
build-args: |
CUSTOM_GITHUB_TOKEN=${{ secrets.CUSTOM_GITHUB_TOKEN }}
BRANCH_NAME=main
file: docker/ee/ee-production.Dockerfile
push: true
tags: tooljet/tooljet-ee:${{ github.event.release.tag_name }},tooljet/tooljet-ee:ee-lts-latest,tooljet/tooljet:ee-lts-latest,tooljet/tooljet:${{ github.event.release.tag_name }}
tags: tooljet/tooljet-ee:${{ github.event.release.tag_name }},tooljet/tooljet-ee:ee-latest,tooljet/tooljet:ee-latest,tooljet/tooljet:${{ github.event.release.tag_name }}
platforms: linux/amd64
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and Push Docker image for LTS tag
if: "contains(github.event.release.tag_name, '-ee-lts')"
uses: docker/build-push-action@v4
@ -155,6 +155,7 @@ jobs:
context: .
build-args: |
CUSTOM_GITHUB_TOKEN=${{ secrets.CUSTOM_GITHUB_TOKEN }}
BRANCH_NAME=lts-3.6
file: docker/ee/ee-production.Dockerfile
push: true
tags: tooljet/tooljet-ee:${{ github.event.release.tag_name }},tooljet/tooljet-ee:ee-lts-latest,tooljet/tooljet:ee-lts-latest,tooljet/tooljet:${{ github.event.release.tag_name }}
@ -174,64 +175,64 @@ jobs:
curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }}
# commented out for now, since cloud modularisation is not yet ready
build-tooljet-image-for-cloud-edtion:
# build-tooljet-image-for-cloud-edtion:
runs-on: ubuntu-latest
if: "${{ github.event.release }}"
# runs-on: ubuntu-latest
# if: "${{ github.event.release }}"
steps:
- name: Checkout code to LTS for Cloud LTS edition
if: "contains(github.event.release.tag_name, '-cloud-lts')"
uses: actions/checkout@v2
with:
ref: refs/heads/lts-3.6
# steps:
# - name: Checkout code to LTS for Cloud LTS edition
# if: "contains(github.event.release.tag_name, '-cloud-lts')"
# uses: actions/checkout@v2
# with:
# ref: refs/heads/lts-4.0
# Create Docker Buildx builder with platform configuration
- name: Set up Docker Buildx
run: |
mkdir -p ~/.docker/cli-plugins
curl -SL https://github.com/docker/buildx/releases/download/v0.11.0/buildx-v0.11.0.linux-amd64 -o ~/.docker/cli-plugins/docker-buildx
chmod a+x ~/.docker/cli-plugins/docker-buildx
docker buildx create --name mybuilder --platform linux/arm64,linux/amd64,linux/amd64/v2,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
docker buildx use mybuilder
# # Create Docker Buildx builder with platform configuration
# - name: Set up Docker Buildx
# run: |
# mkdir -p ~/.docker/cli-plugins
# curl -SL https://github.com/docker/buildx/releases/download/v0.11.0/buildx-v0.11.0.linux-amd64 -o ~/.docker/cli-plugins/docker-buildx
# chmod a+x ~/.docker/cli-plugins/docker-buildx
# docker buildx create --name mybuilder --platform linux/arm64,linux/amd64,linux/amd64/v2,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
# docker buildx use mybuilder
- name: Set DOCKER_CLI_EXPERIMENTAL
run: echo "DOCKER_CLI_EXPERIMENTAL=enabled" >> $GITHUB_ENV
# - name: Set DOCKER_CLI_EXPERIMENTAL
# run: echo "DOCKER_CLI_EXPERIMENTAL=enabled" >> $GITHUB_ENV
- name: use mybuilder buildx
run: docker buildx use mybuilder
# - name: use mybuilder buildx
# run: docker buildx use mybuilder
- name: Docker Login
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
# - name: Docker Login
# uses: docker/login-action@v2
# with:
# username: ${{ secrets.DOCKER_USERNAME }}
# password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and Push Docker image for LTS tag
if: "contains(github.event.release.tag_name, '-cloud-lts')"
uses: docker/build-push-action@v4
with:
context: .
build-args: |
CUSTOM_GITHUB_TOKEN=${{ secrets.CUSTOM_GITHUB_TOKEN }}
BRANCH_NAME=lts-3.6
file: docker/cloud/cloud-server.Dockerfile
push: true
tags: tooljet/saas:${{ github.event.release.tag_name }}
platforms: linux/amd64
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
# - name: Build and Push Docker image for LTS tag
# if: "contains(github.event.release.tag_name, '-cloud-lts')"
# uses: docker/build-push-action@v4
# with:
# context: .
# args: ${{ secrets.CUSTOM_GITHUB_TOKEN }}
# file: docker/cloud/cloud-server.Dockerfile
# push: true
# tags: tooljet/saas:${{ github.event.release.tag_name }}
# platforms: linux/amd64
# env:
# DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
# DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
- name: Send Slack Notification
run: |
if [[ "${{ job.status }}" == "success" ]]; then
message="ToolJet cloud image published:\n\`tooljet/saas:${{ github.event.release.tag_name }}\`"
else
message="Job '${{ env.JOB_NAME }}' failed! Image built:\n\`tooljet/saas:${{ github.event.release.tag_name }}\`"
fi
# - name: Send Slack Notification
# run: |
# if [[ "${{ job.status }}" == "success" ]]; then
# message="ToolJet cloud image published:\n\`tooljet/saas:${{ github.event.release.tag_name }}\`"
# else
# message="Job '${{ env.JOB_NAME }}' failed! Image built:\n\`tooljet/saas:${{ github.event.release.tag_name }}\`"
# fi
# curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }}
curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }}
try-tooljet-image-build:

View file

@ -0,0 +1,57 @@
name: Manual Docker Build and Push
on:
workflow_dispatch:
inputs:
branch_name:
description: 'Git branch to build from'
required: true
default: 'main'
dockerfile_path:
description: 'Path to Dockerfile'
required: true
docker_tag:
description: 'Docker tag suffix (e.g., pre-release-14)'
required: true
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.branch_name }}
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Generate full Docker tag
id: taggen
run: |
input_tag="${{ github.event.inputs.docker_tag }}"
if [[ "$input_tag" == *"/"* ]]; then
echo "tag=$input_tag" >> $GITHUB_OUTPUT
else
echo "tag=tooljet/tj-osv:$input_tag" >> $GITHUB_OUTPUT
fi
- name: Build and Push Docker image
uses: docker/build-push-action@v4
with:
context: .
file: ${{ github.event.inputs.dockerfile_path }}
push: true
tags: ${{ steps.taggen.outputs.tag }}
platforms: linux/amd64
build-args: |
CUSTOM_GITHUB_TOKEN=${{ secrets.CUSTOM_GITHUB_TOKEN }}
BRANCH_NAME=${{ github.event.inputs.branch_name }}

View file

@ -47,7 +47,6 @@ jobs:
- name: Checkout base repo
uses: actions/checkout@v4
with:
repository: ToolJet/ToolJet
token: ${{ secrets.TOKEN_PR }}
ref: main
submodules: recursive
@ -63,13 +62,30 @@ jobs:
git add frontend/ee server/ee
if git diff --cached --quiet; then
echo "No submodule updates found."
else
git commit -m "🔄 chore: update submodules to latest main after auto-merge"
git push origin main
echo "No submodule updates found." && exit 0
fi
env:
GH_TOKEN: ${{ secrets.TOKEN_PR }}
GH_TOKEN: ${{ secrets.TOKEN_PR }}
- name: Create PR for submodule update
id: cpr
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.TOKEN_PR }}
commit-message: "🚀 chore: update submodules to latest main after auto-merge"
title: "🚀 chore: update submodules"
body: "Auto-generated PR to update submodules after base PR merge"
branch: auto/update-submodules-${{ github.run_id }}
base: main
- name: Auto-merge PR
if: steps.cpr.outputs.pull-request-number != ''
run: |
echo "Merging submodule update PR #${PR_NUMBER}"
gh pr merge --squash --admin "$PR_NUMBER" --repo ToolJet/ToolJet
env:
GH_TOKEN: ${{ secrets.TOKEN_PR }}
PR_NUMBER: ${{ steps.cpr.outputs.pull-request-number }}
check-submodule-prs:
if: github.event.action == 'labeled' && github.event.label.name == 'ready-to-merge'

View file

@ -16,11 +16,11 @@ jobs:
name: packer-ee
steps:
- name: Checkout code to lts-4.0
- name: Checkout code to lts-3.6 branch
if: contains(github.event.release.tag_name, '-ee-lts')
uses: actions/checkout@v2
with:
ref: refs/heads/lts-4.0
ref: refs/heads/lts-3.6
- name: Setting tag
if: "${{ github.event.inputs.version != '' }}"
@ -69,7 +69,7 @@ jobs:
with:
command: build
#The the below argument is specific for building EE AMI image
arguments: -color=false -on-error=abort -var ami_name=tooljet_${{ env.RELEASE_VERSION }}.ubuntu_focal
arguments: -color=false -on-error=abort -var ami_name=tooljet_${{ env.RELEASE_VERSION }}.ubuntu_jammy
target: .
working_directory: deploy/ec2/ee
env:
@ -78,9 +78,9 @@ jobs:
- name: Send Slack Notification
run: |
if [[ "${{ job.status }}" == "success" ]]; then
message="ToolJet enterprise AWS AMI published:\\n\`tooljet_${{ env.RELEASE_VERSION }}.ubuntu_focal\`"
message="ToolJet enterprise AWS AMI published:\\n\`tooljet_${{ env.RELEASE_VERSION }}.ubuntu-jammy\`"
else
message="ToolJet enterprise AWS AMI release failed! \\n\`tooljet_${{ env.RELEASE_VERSION }}.ubuntu_focal\`"
message="ToolJet enterprise AWS AMI release failed! \\n\`tooljet_${{ env.RELEASE_VERSION }}.ubuntu-jammy\`"
fi
curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }}

2
.nvmrc
View file

@ -1 +1 @@
v18.18.2
v22.15.1

View file

@ -17,6 +17,9 @@
/package.json @shah21 @gsmithun4 @adishm98
/package-lock.json @shah21 @gsmithun4 @adishm98
# Server service files
/server/src/services/email.service.ts @shah21 @gsmithun4
/server/src/mails @shah21 @gsmithun4
# Code owners for all module.ts files
**/module.ts @shah21 @gsmithun4
# Server migration directories
/server/migrations/* @shah21 @gsmithun4
/server/data-migrations/* @shah21 @gsmithun4

View file

@ -98,8 +98,10 @@ module.exports = defineConfig({
configFile: environment.configFile,
specPattern: [
"cypress/e2e/happyPath/platform/firstUser/firstUserOnboarding.cy.js",
"cypress/e2e/happyPath/platform/ceTestcases/apps/appSlug.cy.js",
"cypress/e2e/happyPath/platform/ceTestcases/apps/!(*appSlug).cy.js",
"cypress/e2e/happyPath/platform/commonTestcases/userManagment/*.cy.js",
"cypress/e2e/happyPath/platform/eeTestcases/**/*.cy.js",
"cypress/e2e/happyPath/platform/eeTestcases/workspace/*.cy.js",
],
numTestsKeptInMemory: 1,
redirectionLimit: 15,

View file

@ -77,7 +77,7 @@ module.exports = defineConfig({
baseUrl: "http://localhost:8082",
specPattern: [
"cypress/e2e/happyPath/marketplace/commonTestcases/**/*.cy.js",
],
]
numTestsKeptInMemory: 1,
redirectionLimit: 7,
experimentalRunAllSpecs: true,

View file

@ -22,7 +22,7 @@ RUN git checkout ${BRANCH_NAME}
RUN git submodule update --init --recursive
# Checkout the same branch in submodules if it exists, otherwise stay on default branch
RUN git submodule foreach 'git checkout ${BRANCH_NAME} || true'
RUN git submodule foreach 'git checkout ${BRANCH_NAME}'
# Scripts for building
COPY ./package.json ./package.json
@ -54,7 +54,7 @@ RUN npm install -g @nestjs/cli
RUN npm install -g copyfiles
RUN npm --prefix server run build
FROM node:22.15.1
FROM node:22.15.1-bullseye
RUN apt-get update -yq \
&& apt-get install curl wget gnupg zip -yq \

View file

@ -479,24 +479,22 @@ Cypress.Commands.add("apiMakeAppPublic", (appId = Cypress.env("appId")) => {
});
});
Cypress.Commands.add("apiDeleteGranularPermission", (groupName) => {
Cypress.Commands.add("apiDeleteGranularPermission", (groupName, typesToDelete = []) => {
cy.getAuthHeaders().then((headers) => {
// Fetch group permissions
// Step 1: Get the group by name
cy.request({
method: "GET",
url: `${Cypress.env("server_host")}/api/v2/group-permissions`,
headers: headers,
headers,
log: false,
}).then((response) => {
expect(response.status).to.equal(200);
const group = response.body.groupPermissions.find(
(g) => g.name === groupName
);
const group = response.body.groupPermissions.find((g) => g.name === groupName);
if (!group) throw new Error(`Group with name ${groupName} not found`);
const groupId = group.id;
// Fetch granular permissions for the specific group
// Step 2: Get all granular permissions for the group
cy.request({
method: "GET",
url: `${Cypress.env("server_host")}/api/v2/group-permissions/${groupId}/granular-permissions`,
@ -504,22 +502,31 @@ Cypress.Commands.add("apiDeleteGranularPermission", (groupName) => {
log: false,
}).then((granularResponse) => {
expect(granularResponse.status).to.equal(200);
const granularPermissionId = granularResponse.body[0].id;
const granularPermissions = granularResponse.body;
// Delete the granular permission
cy.request({
method: "DELETE",
url: `${Cypress.env("server_host")}/api/v2/group-permissions/granular-permissions/app/${granularPermissionId}`,
headers,
log: false,
}).then((deleteResponse) => {
expect(deleteResponse.status).to.equal(200);
// Step 3: Filter if typesToDelete is specified
const permissionsToDelete = typesToDelete.length
? granularPermissions.filter((perm) => typesToDelete.includes(perm.type))
: granularPermissions;
// Step 4: Delete each granular permission
permissionsToDelete.forEach((permission) => {
cy.request({
method: "DELETE",
url: `${Cypress.env("server_host")}/api/v2/group-permissions/granular-permissions/app/${permission.id}`,
headers,
log: false,
}).then((deleteResponse) => {
expect(deleteResponse.status).to.equal(200);
cy.log(`Deleted granular permission: ${permission.name}`);
});
});
});
});
});
});
Cypress.Commands.add(
"apiCreateGranularPermission",
(

View file

@ -84,7 +84,20 @@ Cypress.Commands.add(
const dataTransfer = new DataTransfer();
cy.forceClickOnCanvas();
cy.clearAndType(commonSelectors.searchField, widgetName);
cy.get("body")
.then(($body) => {
const isSearchVisible = $body
.find(commonSelectors.searchField)
.is(":visible");
if (!isSearchVisible) {
cy.get('[data-cy="right-sidebar-plus-button"]').click();
}
})
.then(() => {
cy.clearAndType(commonSelectors.searchField, widgetName);
});
cy.get(commonWidgetSelector.widgetBox(widgetName2)).trigger(
"dragstart",
{ dataTransfer },
@ -226,9 +239,9 @@ Cypress.Commands.add(
.invoke("text")
.then((text) => {
cy.wrap(subject).realType(createBackspaceText(text)),
{
delay: 0,
};
{
delay: 0,
};
});
}
);
@ -548,7 +561,7 @@ Cypress.Commands.add("installMarketplacePlugin", (pluginName) => {
}
});
function installPlugin (pluginName) {
function installPlugin(pluginName) {
cy.get('[data-cy="-list-item"]').eq(1).click();
cy.wait(1000);
@ -608,6 +621,7 @@ Cypress.Commands.add("uninstallMarketplacePlugin", (pluginName) => {
Cypress.Commands.add(
"verifyRequiredFieldValidation",
(fieldName, expectedColor) => {
cy.get(commonSelectors.textField(fieldName)).type("some text").clear();
cy.get(commonSelectors.textField(fieldName)).should(
"have.css",
"border-color",
@ -622,11 +636,11 @@ Cypress.Commands.add(
}
);
Cypress.Commands.add('ifEnv', (expectedEnvs, callback) => {
Cypress.Commands.add("ifEnv", (expectedEnvs, callback) => {
const actualEnv = Cypress.env("environment");
const envArray = Array.isArray(expectedEnvs) ? expectedEnvs : [expectedEnvs];
if (envArray.includes(actualEnv)) {
callback();
}
});
});

View file

@ -177,7 +177,7 @@ export const commonSelectors = {
breadcrumbPageTitle: '[data-cy="breadcrumb-page-title"]',
labelFullNameInput: '[data-cy="name-label"]',
duplicateOption: '[data-cy="duplicate-group-card-option"]',
confirmDuplicateButton: '[data-cy="confim-button"]',
confirmDuplicateButton: '[data-cy="confirm-button"]',
inputFieldFullName: '[data-cy="name-input"]',
labelEmailInput: '[data-cy="email-label"]',
inputFieldEmailAddress: '[data-cy="email-input"]',
@ -288,6 +288,7 @@ export const commonSelectors = {
labelFieldAlert: (fieldName) => {
return `[data-cy="${cyParamName(fieldName)}-is-required-field-alert-text"]`;
},
pageLogo: '[data-cy="page-logo"]',
};
export const commonWidgetSelector = {

View file

@ -133,7 +133,7 @@ export const groupsSelector = {
usersCheckInput: '[data-cy="users-check-input"]',
permissionCheckInput: '[data-cy="permissions-check-input"]',
appsCheckInput: '[data-cy="apps-check-input"]',
confimButton: '[data-cy="confim-button"]',
confimButton: '[data-cy="confirm-button"]',
duplicatedGroupLink: (groupName) => {
return `[data-cy="${cyParamName(groupName)}_copy-list-item"]`
},

View file

@ -202,10 +202,10 @@ describe("Data source Airtable", () => {
);
cy.get(dataSourceSelector.queryPreviewButton).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
`Query (${data.dsName}) completed.`
);
// cy.verifyToastMessage(
// commonSelectors.toastMessage,
// `Query (${data.dsName}) completed.`
// );
// Verfiy Retrieve record operation
@ -225,10 +225,10 @@ describe("Data source Airtable", () => {
);
cy.get(dataSourceSelector.queryPreviewButton).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
`Query (${data.dsName}) completed.`
);
// cy.verifyToastMessage(
// commonSelectors.toastMessage,
// `Query (${data.dsName}) completed.`
// );
// Verfiy Create record operation
@ -251,10 +251,10 @@ describe("Data source Airtable", () => {
.realType('": {}', { force: true, delay: 0 });
cy.get(dataSourceSelector.queryPreviewButton).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
`Query (${data.dsName}) completed.`
);
// cy.verifyToastMessage(
// commonSelectors.toastMessage,
// `Query (${data.dsName}) completed.`
// );
// Verfiy Update record operation
@ -285,10 +285,10 @@ describe("Data source Airtable", () => {
.realType('"Phone Number": "555_98"', { force: true, delay: 0 });
cy.get(dataSourceSelector.queryPreviewButton).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
`Query (${data.queryName}) completed.`
);
// cy.verifyToastMessage(
// commonSelectors.toastMessage,
// `Query (${data.queryName}) completed.`
// );
// Verify Delete record operation
@ -337,10 +337,10 @@ describe("Data source Airtable", () => {
);
cy.get(dataSourceSelector.queryPreviewButton).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
`Query (${data.queryName}) completed.`
);
// cy.verifyToastMessage(
// commonSelectors.toastMessage,
// `Query (${data.queryName}) completed.`
// );
cy.apiDeleteApp(`${data.dsName}-airtable-app`);
cy.apiDeleteGDS(`cypress-${data.dsName}-airtable`);

View file

@ -254,7 +254,7 @@ describe("Data sources", () => {
.and("be.disabled");
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
"have.text",
"connect ECONNREFUSED 127.0.0.1:5432"
postgreSqlText.serverNotSuppotSsl
);
cy.apiDeleteGDS(`cypress-${data.dataSourceName}-postgresql`);

View file

@ -22,7 +22,7 @@ describe("App Import Functionality", () => {
let data;
beforeEach(() => {
cy.viewport(1200, 1300);
cy.viewport(1400, 1400);
data = {
workspaceName: fake.firstName,
workspaceSlug: fake.firstName.toLowerCase().replace(/\s+/g, "-"),
@ -34,7 +34,7 @@ describe("App Import Functionality", () => {
cy.apiLogin();
cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug);
cy.apiLogout();
cy.skipWalkthrough()
cy.skipWalkthrough();
});
it("should verify app import functionality", () => {
@ -151,23 +151,49 @@ describe("App Import Functionality", () => {
cy.visit(`${data.workspaceSlug}/data-sources`);
cy.get('[data-cy="postgresql-button"]').should("be.visible");
cy.apiUpdateDataSource("postgresql", "production", {
options: [
{
key: "password",
value: `${Cypress.env("pg_password")}`,
encrypted: true,
},
],
cy.ifEnv("Community", () => {
cy.apiUpdateDataSource("postgresql", "production", {
options: [
{
key: "password",
value: `${Cypress.env("pg_password")}`,
encrypted: true,
},
],
});
});
cy.ifEnv("Enterprise", () => {
cy.apiUpdateDataSource("postgresql", "development", {
options: [
{
key: "password",
value: `${Cypress.env("pg_password")}`,
encrypted: true,
},
],
});
});
cy.apiCreateWsConstant(
"pageHeader",
"Import and Export",
["Global"],
["production"]
);
cy.apiCreateWsConstant("db_name", "persons", ["Secret"], ["production"]);
cy.ifEnv("Community", () => {
cy.apiCreateWsConstant(
"pageHeader",
"Import and Export",
["Global"],
["production"]
);
cy.apiCreateWsConstant("db_name", "persons", ["Secret"], ["production"]);
});
cy.ifEnv("Enterprise", () => {
cy.apiCreateWsConstant(
"pageHeader",
"Import and Export",
["Global"],
["development"]
);
cy.apiCreateWsConstant("db_name", "persons", ["Secret"], ["development"]);
});
// Verify app after setup
cy.wait("@importApp").then((interception) => {

View file

@ -7,6 +7,7 @@ import {
verifyURLs,
resolveHost,
} from "Support/utils/apps";
import { appPromote } from "Support/utils/platform/multiEnv";
describe("App Slug", () => {
const data = {};
@ -153,6 +154,7 @@ describe("App Slug", () => {
cy.visit("/my-workspace");
cy.apiCreateApp(data.slug);
cy.openApp("my-workspace");
releaseApp();
cy.get(commonWidgetSelector.shareAppButton).click();
cy.clearAndType(commonWidgetSelector.appNameSlugInput, data.slug);

View file

@ -12,6 +12,8 @@ import {
verifyRestrictedAccess,
onboardUserFromAppLink,
} from "Support/utils/apps";
import { appPromote } from "Support/utils/platform/multiEnv";
import { InstanceSSO } from "Support/utils/platform/eeCommon";
describe(
"Private and Public apps",
@ -98,7 +100,7 @@ describe(
);
// Test public access
cy.get(commonSelectors.viewerPageLogo).click();
// cy.get(commonSelectors.viewerPageLogo).click();
cy.openApp(
"appSlug",
Cypress.env("workspaceId"),
@ -150,7 +152,7 @@ describe(
"be.visible"
);
cy.get(commonSelectors.viewerPageLogo).click();
// cy.get(commonSelectors.viewerPageLogo).click();
// Test public access
cy.defaultWorkspaceLogin();
@ -183,6 +185,9 @@ describe(
setupAppWithSlug(data.appName, data.slug);
cy.apiLogout();
cy.ifEnv("Enterprise", () => {
InstanceSSO(true, true, true);
});
userSignUp(data.firstName, data.email, data.workspaceName);
cy.wait(1000);
cy.visitSlug({
@ -253,7 +258,9 @@ describe(
"be.visible"
);
cy.get('[data-cy="viewer-page-logo"]').click();
// cy.get('[data-cy="viewer-page-logo"]').click();
cy.visit("/my-workspace");
cy.wait(2000);
logout();
cy.wait(1000);
cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should(
@ -312,7 +319,9 @@ describe(
cy.apiLogout();
cy.apiLogin();
cy.visit(`${data.workspaceSlug}`);
cy.apiDeleteGranularPermission("end-user");
cy.apiDeleteGranularPermission("end-user", ["app", "workflow"]);
setSignupStatus(true, data.workspaceName);
setupAppWithSlug(data.appName, data.slug);

View file

@ -1,7 +1,6 @@
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
import { fake } from "Fixtures/fake";
import { commonText } from "Texts/common";
import {
editVersionAndVerify,
deleteVersionAndVerify,
@ -13,25 +12,20 @@ import {
navigateToEditVersionModal,
switchVersionAndVerify,
} from "Support/utils/version";
import { appVersionSelectors } from "Selectors/exportImport";
import { editVersionSelectors } from "Selectors/version";
import { editVersionText } from "Texts/version";
import { createNewVersion } from "Support/utils/exportImport";
import { verifyModal, closeModal } from "Support/utils/common";
import {
verifyComponent,
verifyComponentinrightpannel,
deleteComponentAndVerify,
} from "Support/utils/basicComponents";
import { deleteVersionText, onlydeleteVersionText } from "Texts/version";
import { createRestAPIQuery } from "Support/utils/dataSource";
import { deleteQuery } from "Support/utils/queries";
import { selectEnv, appPromote } from "Support/utils/platform/multiEnv";
describe("App Version", () => {
let data;
@ -50,6 +44,8 @@ describe("App Version", () => {
cy.defaultWorkspaceLogin();
cy.apiCreateApp(data.appName);
cy.openApp();
cy.viewport(1400, 1400);
});
it("should verify basic version management operations", () => {
@ -120,7 +116,15 @@ describe("App Version", () => {
// Preview and release verification
cy.openInCurrentTab(commonWidgetSelector.previewButton);
cy.url().should("include", "/home?version=v2");
cy.ifEnv("Community", () => {
cy.url().should("include", "/home?version=v2");
});
cy.ifEnv("Enterprise", () => {
cy.url().should("include", "/home?env=development&version=v2");
});
cy.openApp(
"",
Cypress.env("workspaceId"),
@ -149,7 +153,11 @@ describe("App Version", () => {
createRestAPIQuery(data.query1, data.datasourceName, "", "", "/1", true);
// Version v2 creation and verification
cy.ifEnv("Enterprise", () => {
appPromote("development", "production");
});
// Version v2 creation and verification and v2 is created from v1 production environment
navigateToCreateNewVersionModal("v1");
createNewVersion(["v2"], "v1");
cy.get(commonWidgetSelector.draggableWidget("text1")).verifyVisibleElement(
@ -201,7 +209,8 @@ describe("App Version", () => {
versionChecks.forEach((check) => {
navigateToCreateNewVersionModal(check.create.from);
createNewVersion([check.create.version], check.create.from);
cy.waitForAutoSave();
cy.wait(1000);
if (check.verify.component.value) {
cy.get(
commonWidgetSelector.draggableWidget(check.verify.component.selector)
@ -224,6 +233,9 @@ describe("App Version", () => {
);
// Version switching and component verification
cy.ifEnv("Enterprise", () => {
selectEnv("development");
});
cy.get(appVersionSelectors.currentVersionField("v5")).click();
cy.contains(`[id*="react-select-"]`, "v4").click();
cy.get(appVersionSelectors.currentVersionField("v4")).should(
@ -238,7 +250,14 @@ describe("App Version", () => {
// Preview and version switching verification
cy.openInCurrentTab(commonWidgetSelector.previewButton);
cy.url().should("include", "/home?version=v4");
cy.ifEnv("Community", () => {
cy.url().should("include", "/home?version=v4");
});
cy.ifEnv("Enterprise", () => {
cy.url().should("include", "/home?env=development&version=v4");
});
cy.get(commonWidgetSelector.draggableWidget("text1")).verifyVisibleElement(
"have.text",
"Leanne Graham"
@ -250,8 +269,74 @@ describe("App Version", () => {
cy.get(
commonWidgetSelector.draggableWidget("textInput")
).verifyVisibleElement("have.value", "Ervin Howell");
//url validation should be added after bug fix
// cy.url().should("include", "/home?version=v5");
cy.ifEnv("Enterprise", () => {
cy.openApp(
"",
Cypress.env("workspaceId"),
Cypress.env("appId"),
commonWidgetSelector.draggableWidget("textInput")
);
navigateToCreateNewVersionModal("v5");
createNewVersion(["v6"], "v5");
cy.waitForAutoSave();
cy.wait(1000);
appPromote("development", "staging");
cy.get(
commonWidgetSelector.draggableWidget("textInput")
).verifyVisibleElement("have.value", "Ervin Howell");
cy.get(`[data-cy="list-query-${data.query2}"]`).should("be.visible");
appPromote("staging", "production");
cy.get(
commonWidgetSelector.draggableWidget("textInput")
).verifyVisibleElement("have.value", "Ervin Howell");
cy.get(`[data-cy="list-query-${data.query2}"]`).should("be.visible");
cy.openInCurrentTab(commonWidgetSelector.previewButton);
cy.get(
commonWidgetSelector.draggableWidget("textInput")
).verifyVisibleElement("have.value", "Ervin Howell");
cy.url().should("include", "/home?env=production&version=v6");
cy.wait(1000);
cy.get('[data-cy="preview-settings"]').click();
switchVersionAndVerify("v6", "v1");
cy.get(
commonWidgetSelector.draggableWidget("text1")
).verifyVisibleElement("have.text", "Leanne Graham");
// url bug
// cy.url().should("include", "/home?env=production&version=v1");
cy.wait(1000);
cy.get('[data-cy="preview-settings"]').click();
switchVersionAndVerify("v1", "v6");
cy.wait(1000);
cy.get('[data-cy="preview-settings"]').click();
selectEnv("staging");
cy.get(
commonWidgetSelector.draggableWidget("textInput")
).verifyVisibleElement("have.value", "Ervin Howell");
// cy.url().should("include", "/home?env=staging&version=v6");
cy.wait(1000);
cy.get('[data-cy="preview-settings"]').click();
selectEnv("development");
cy.wait(1000);
cy.get('[data-cy="preview-settings"]').click();
switchVersionAndVerify("v6", "v1");
cy.get(
commonWidgetSelector.draggableWidget("text1")
).verifyVisibleElement("have.text", "Leanne Graham");
});
});
});

View file

@ -47,8 +47,8 @@ describe("Datasource Manager", () => {
data.dsName1 = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", "");
data.dsName2 = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", "");
const allDataSources = host.includes("8082") ? "All data sources (43)" : "All data sources (45)";
const allDatabase = host.includes("8082") ? "Databases (18)" : "Databases (20)";
const allDataSources = host.includes("8082") ? "All data sources (45)" : "All data sources (45)";
const allDatabase = host.includes("8082") ? "Databases (20)" : "Databases (20)";
cy.get(commonSelectors.globalDataSourceIcon).click();
cy.get(commonSelectors.pageSectionHeader).verifyVisibleElement(

View file

@ -39,6 +39,8 @@ describe("Workspace constants", () => {
beforeEach(() => {
cy.defaultWorkspaceLogin();
cy.skipWalkthrough();
cy.viewport(1800, 1800);
});
it("Verify workspace constants UI and CRUD operations", () => {
@ -66,12 +68,11 @@ describe("Workspace constants", () => {
});
});
it("Verify global and secret constants in the editor, inspector, data sources, static queries, query preview, and preview", () => {
it.only("Verify global and secret constants in the editor, inspector, data sources, static queries, query preview, and preview", () => {
data.workspaceName = fake.firstName;
data.workspaceSlug = fake.firstName.toLowerCase().replace(/[^A-Za-z]/g, "");
cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug);
cy.visit(data.workspaceSlug);
cy.viewport(1440, 960);
data.appName = `${fake.companyName}-App`;
// create global constants
@ -102,8 +103,8 @@ describe("Workspace constants", () => {
.eq(0)
.selectFile('cypress/fixtures/templates/workspace_constants.json', { force: true });
cy.get(importSelectors.importAppButton).click();
cy.wait(5000);
cy.wait(6000);
cy.get(commonWidgetSelector.draggableWidget('textinput1')).should('be.visible');
//Verify global constant value is resolved in component
cy.get(commonWidgetSelector.draggableWidget('textinput1'))
.verifyVisibleElement("have.value", "customHeader");
@ -115,9 +116,10 @@ describe("Workspace constants", () => {
cy.get(commonWidgetSelector.alertInfoText).contains(
"secrets cannot be used in apps"
);
//Verify all static and datasource queries output in components
cy.wait(8000);
for (let i = 3; i <= 16; i++) {
cy.wait(1000);
cy.log("Verifying textinput" + i);
cy.get(commonWidgetSelector.draggableWidget(`textinput${i}`))
.verifyVisibleElement("have.value", "Production environment testing");
@ -151,16 +153,20 @@ describe("Workspace constants", () => {
//Preview app and verify components
cy.openInCurrentTab(commonWidgetSelector.previewButton);
cy.wait(6000);
for (let i = 3; i <= 16; i++) {
cy.wait(8000);
cy.get(commonWidgetSelector.draggableWidget('textinput1')).should('be.visible');
for (let i = 16; i >= 3; i--) {
cy.wait(1000);
cy.get(commonWidgetSelector.draggableWidget(`textinput${i}`)).should('be.visible');
cy.get(commonWidgetSelector.draggableWidget(`textinput${i}`))
.verifyVisibleElement("have.value", "Production environment testing");
.verifyVisibleElement("have.value", "Production environment testing", { timeout: 10000 });
}
//back to dashboard and open app again
cy.get(commonSelectors.viewerPageLogo).click();
cy.wait(2000);
cy.visit('/');
cy.wait(4000);
cy.get(commonSelectors.appEditButton).click({ force: true });
cy.wait(4000);
cy.releaseApp();

View file

@ -369,7 +369,7 @@ describe("user invite flow cases", () => {
"have.text",
"Cancel"
);
cy.get('[data-cy="confim-button"]').verifyVisibleElement(
cy.get('[data-cy="confirm-button"]').verifyVisibleElement(
"have.text",
"Continue"
);
@ -407,7 +407,7 @@ describe("user invite flow cases", () => {
cy.get('[data-cy="group-check-input"]').eq(0).check();
cy.get(usersSelector.buttonInviteUsers).click();
cy.get('[data-cy="confim-button"]').click();
cy.get('[data-cy="confirm-button"]').click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
@ -426,7 +426,7 @@ describe("user invite flow cases", () => {
cy.get('[data-cy="group-check-input"]').eq(0).check();
cy.get(usersSelector.buttonInviteUsers).click();
cy.get('[data-cy="confim-button"]').click();
cy.get('[data-cy="confirm-button"]').click();
cy.verifyToastMessage(
commonSelectors.toastMessage,

View file

@ -24,6 +24,7 @@ import { logout } from "Support/utils/common";
describe("dashboard", () => {
let data = {};
beforeEach(() => {
data = {
appName: `${fake.companyName}-App`,
@ -44,164 +45,6 @@ describe("dashboard", () => {
cy.visit(`${data.workspaceSlug}`);
});
// it("Should verify app card elements and app card operations", () => {
// const customLayout = {
// desktop: { top: 100, left: 20 },
// mobile: { width: 8, height: 50 },
// };
// cy.apiCreateApp(data.appName);
// cy.visit(`${data.workspaceSlug}`);
// cy.wait(2000);
// cy.get(commonSelectors.appCreationDetails).should("be.visible");
// cy.get(commonSelectors.appCard(data.appName)).should("be.visible");
// cy.get(commonSelectors.appTitle(data.appName)).verifyVisibleElement(
// "have.text",
// data.appName
// );
// viewAppCardOptions(data.appName);
// cy.get(
// commonSelectors.appCardOptions(commonText.changeIconOption)
// ).verifyVisibleElement("have.text", commonText.changeIconOption);
// cy.get(
// commonSelectors.appCardOptions(commonText.addToFolderOption)
// ).verifyVisibleElement("have.text", commonText.addToFolderOption);
// cy.get(
// commonSelectors.appCardOptions(commonText.cloneAppOption)
// ).verifyVisibleElement("have.text", commonText.cloneAppOption);
// cy.get(
// commonSelectors.appCardOptions(commonText.exportAppOption)
// ).verifyVisibleElement("have.text", commonText.exportAppOption);
// cy.get(
// commonSelectors.appCardOptions(commonText.deleteAppOption)
// ).verifyVisibleElement("have.text", commonText.deleteAppOption);
// modifyAndVerifyAppCardIcon(data.appName);
// createFolder(data.folderName);
// viewAppCardOptions(data.appName);
// cy.get(
// commonSelectors.appCardOptions(commonText.addToFolderOption)
// ).click();
// verifyModal(
// dashboardText.addToFolderTitle,
// dashboardText.addToFolderButton,
// dashboardSelector.selectFolder
// );
// cy.get(dashboardSelector.moveAppText).verifyVisibleElement(
// "have.text",
// dashboardText.moveAppText(data.appName)
// );
// cy.get(dashboardSelector.selectFolder).click();
// cy.get(commonSelectors.folderList).contains(data.folderName).click();
// cy.get(dashboardSelector.addToFolderButton).click();
// cy.verifyToastMessage(
// commonSelectors.toastMessage,
// commonText.AddedToFolderToast,
// false
// );
// cy.get(dashboardSelector.folderName(data.folderName)).verifyVisibleElement(
// "have.text",
// dashboardText.folderName(`${data.folderName} (1)`)
// );
// cy.get(dashboardSelector.folderName(data.folderName)).click();
// cy.get(commonSelectors.appCard(data.appName))
// .contains(data.appName)
// .should("be.visible");
// viewAppCardOptions(data.appName);
// cy.get(commonSelectors.appCardOptions(commonText.removeFromFolderOption))
// .verifyVisibleElement("have.text", commonText.removeFromFolderOption)
// .click();
// verifyConfirmationModal(commonText.appRemovedFromFolderMessage);
// cancelModal(commonText.cancelButton);
// viewAppCardOptions(data.appName);
// cy.get(
// commonSelectors.appCardOptions(commonText.removeFromFolderOption)
// ).click();
// cy.get(commonSelectors.buttonSelector(commonText.modalYesButton)).click();
// cy.verifyToastMessage(
// commonSelectors.toastMessage,
// commonText.appRemovedFromFolderTaost,
// false
// );
// cy.get(commonSelectors.modalComponent).should("not.exist");
// cy.get(commonSelectors.empytyFolderImage).should("be.visible");
// cy.get(commonSelectors.emptyFolderText).verifyVisibleElement(
// "have.text",
// commonText.emptyFolderText
// );
// cy.get(commonSelectors.allApplicationsLink).click();
// deleteFolder(data.folderName);
// cy.get(commonSelectors.allApplicationsLink).click();
// cy.wait(1000);
// viewAppCardOptions(data.appName);
// cy.wait(2000);
// cy.get(commonSelectors.appCardOptions(commonText.exportAppOption)).click();
// cy.get(commonSelectors.exportAllButton).click();
// cy.exec("ls ./cypress/downloads/").then((result) => {
// const downloadedAppExportFileName = result.stdout.split("\n")[0];
// expect(downloadedAppExportFileName).to.contain.string("app");
// });
// viewAppCardOptions(data.appName);
// cy.get(commonSelectors.appCardOptions(commonText.cloneAppOption)).click();
// cy.get('[data-cy="clone-app"]').click();
// cy.get(".go3958317564")
// .should("be.visible")
// .and("have.text", dashboardText.appClonedToast);
// cy.wait(3000);
// cy.renameApp(data.cloneAppName);
// cy.apiAddComponentToApp(data.cloneAppName, "button", 25, 25);
// cy.backToApps();
// cy.wait("@appLibrary");
// cy.wait(1000);
// cy.get(commonSelectors.appCard(data.cloneAppName)).should("be.visible");
// cy.wait(1000);
// viewAppCardOptions(data.cloneAppName);
// cy.get(commonSelectors.deleteAppOption).click();
// cy.get(commonSelectors.modalMessage).verifyVisibleElement(
// "have.text",
// commonText.deleteAppModalMessage(data.cloneAppName)
// );
// cy.get(
// commonSelectors.buttonSelector(commonText.cancelButton)
// ).verifyVisibleElement("have.text", commonText.cancelButton);
// cy.get(
// commonSelectors.buttonSelector(commonText.modalYesButton)
// ).verifyVisibleElement("have.text", commonText.modalYesButton);
// cancelModal(commonText.cancelButton);
// viewAppCardOptions(data.cloneAppName);
// cy.get(commonSelectors.deleteAppOption).click();
// cy.get(commonSelectors.buttonSelector(commonText.modalYesButton)).click();
// cy.verifyToastMessage(
// commonSelectors.toastMessage,
// commonText.appDeletedToast,
// false
// );
// verifyAppDelete(data.cloneAppName);
// cy.wait("@appLibrary");
// cy.deleteApp(data.appName);
// verifyAppDelete(data.appName);
// });
it("should verify the elements on empty dashboard", () => {
cy.intercept("GET", "/api/metadata", {
body: {
@ -259,9 +102,6 @@ describe("dashboard", () => {
.should("have.attr", "class")
.and("contain", "theme-dark");
cy.get(dashboardSelector.modeToggle).click();
cy.get(dashboardSelector.homePageContent)
.should("have.attr", "class")
.and("contain", "bg-light-gray");
cy.wait(500);
cy.get(commonSelectors.settingsIcon).click();
@ -329,6 +169,169 @@ describe("dashboard", () => {
verifyTooltip(dashboardSelector.modeToggle, "Mode");
});
it("Should verify app card elements and app card operations", () => {
cy.exec("mkdir -p ./cypress/downloads/");
cy.exec("cd ./cypress/downloads/ && rm -rf *");
const customLayout = {
desktop: { top: 100, left: 20 },
mobile: { width: 8, height: 50 },
};
cy.apiCreateApp(data.appName);
cy.visit(`${data.workspaceSlug}`);
cy.wait(2000);
cy.get(commonSelectors.appCreationDetails).should("be.visible");
cy.get(commonSelectors.appCard(data.appName)).should("be.visible");
cy.get(commonSelectors.appTitle(data.appName)).verifyVisibleElement(
"have.text",
data.appName
);
viewAppCardOptions(data.appName);
cy.get(
commonSelectors.appCardOptions(commonText.changeIconOption)
).verifyVisibleElement("have.text", commonText.changeIconOption);
cy.get(
commonSelectors.appCardOptions(commonText.addToFolderOption)
).verifyVisibleElement("have.text", commonText.addToFolderOption);
cy.get(
commonSelectors.appCardOptions(commonText.cloneAppOption)
).verifyVisibleElement("have.text", commonText.cloneAppOption);
cy.get(
commonSelectors.appCardOptions(commonText.exportAppOption)
).verifyVisibleElement("have.text", commonText.exportAppOption);
cy.get(
commonSelectors.appCardOptions(commonText.deleteAppOption)
).verifyVisibleElement("have.text", commonText.deleteAppOption);
modifyAndVerifyAppCardIcon(data.appName);
createFolder(data.folderName);
viewAppCardOptions(data.appName);
cy.get(
commonSelectors.appCardOptions(commonText.addToFolderOption)
).click();
verifyModal(
dashboardText.addToFolderTitle,
dashboardText.addToFolderButton,
dashboardSelector.selectFolder
);
cy.get(dashboardSelector.moveAppText).verifyVisibleElement(
"have.text",
dashboardText.moveAppText(data.appName)
);
cy.get(dashboardSelector.selectFolder).click();
cy.get(commonSelectors.folderList).contains(data.folderName).click();
cy.get(dashboardSelector.addToFolderButton).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
commonText.AddedToFolderToast,
false
);
cy.get(dashboardSelector.folderName(data.folderName)).verifyVisibleElement(
"have.text",
dashboardText.folderName(`${data.folderName} (1)`)
);
cy.get(dashboardSelector.folderName(data.folderName)).click();
cy.get(commonSelectors.appCard(data.appName))
.contains(data.appName)
.should("be.visible");
viewAppCardOptions(data.appName);
cy.get(commonSelectors.appCardOptions(commonText.removeFromFolderOption))
.verifyVisibleElement("have.text", commonText.removeFromFolderOption)
.click();
verifyConfirmationModal(commonText.appRemovedFromFolderMessage);
cancelModal(commonText.cancelButton);
viewAppCardOptions(data.appName);
cy.get(
commonSelectors.appCardOptions(commonText.removeFromFolderOption)
).click();
cy.get(commonSelectors.buttonSelector(commonText.modalYesButton)).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
commonText.appRemovedFromFolderTaost,
false
);
cy.get(commonSelectors.modalComponent).should("not.exist");
cy.get(commonSelectors.empytyFolderImage).should("be.visible");
cy.get(commonSelectors.emptyFolderText).verifyVisibleElement(
"have.text",
commonText.emptyFolderText
);
cy.get(commonSelectors.allApplicationsLink).click();
deleteFolder(data.folderName);
cy.get(commonSelectors.allApplicationsLink).click();
cy.wait(1000);
viewAppCardOptions(data.appName);
cy.wait(2000);
cy.get(commonSelectors.appCardOptions(commonText.exportAppOption)).click();
cy.get(commonSelectors.exportAllButton).click();
cy.exec("ls ./cypress/downloads/").then((result) => {
const downloadedAppExportFileName = result.stdout.split("\n")[0];
expect(downloadedAppExportFileName).to.contain.string("app");
});
viewAppCardOptions(data.appName);
cy.get(commonSelectors.appCardOptions(commonText.cloneAppOption)).click();
cy.get('[data-cy="clone-app"]').click();
cy.get(".go3958317564")
.should("be.visible")
.and("have.text", dashboardText.appClonedToast);
cy.wait(3000);
cy.renameApp(data.cloneAppName);
cy.apiAddComponentToApp(data.cloneAppName, "button", 25, 25);
cy.backToApps();
cy.wait("@appLibrary");
cy.wait(1000);
cy.get(commonSelectors.appCard(data.cloneAppName)).should("be.visible");
cy.wait(1000);
viewAppCardOptions(data.cloneAppName);
cy.get(commonSelectors.deleteAppOption).click();
cy.get(commonSelectors.modalMessage).verifyVisibleElement(
"have.text",
commonText.deleteAppModalMessage(data.cloneAppName)
);
cy.get(
commonSelectors.buttonSelector(commonText.cancelButton)
).verifyVisibleElement("have.text", commonText.cancelButton);
cy.get(
commonSelectors.buttonSelector(commonText.modalYesButton)
).verifyVisibleElement("have.text", commonText.modalYesButton);
cancelModal(commonText.cancelButton);
viewAppCardOptions(data.cloneAppName);
cy.get(commonSelectors.deleteAppOption).click();
cy.get(commonSelectors.buttonSelector(commonText.modalYesButton)).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
commonText.appDeletedToast,
false
);
cy.get(commonSelectors.appCard(data.cloneAppName)).should("not.exist");
cy.wait("@appLibrary");
cy.deleteApp(data.appName);
cy.get(commonSelectors.appCard(data.appName)).should("not.exist");
});
it("Should verify the app CRUD operation", () => {
const customLayout = {
desktop: { top: 100, left: 20 },
@ -353,7 +356,7 @@ describe("dashboard", () => {
cy.deleteApp(data.appName);
verifyAppDelete(data.appName);
cy.get(commonSelectors.appCard(data.appName)).should("not.exist");
});
it("Should verify the folder CRUD operation", () => {
@ -474,7 +477,7 @@ describe("dashboard", () => {
cy.get(commonSelectors.allApplicationsLink).click();
cy.deleteApp(data.appName);
verifyAppDelete(data.appName);
cy.get(commonSelectors.appCard(data.appName)).should("not.exist");
logout();
});
});

View file

@ -76,11 +76,11 @@ describe("Manage Groups", () => {
// App operations
cy.createApp(data.appName);
cy.verifyToastMessage(
commonSelectors.toastMessage,
commonText.appCreatedToast,
false
);
// cy.verifyToastMessage(
// commonSelectors.toastMessage,
// commonText.appCreatedToast,
// false
// );
cy.backToApps();
cy.deleteApp(data.appName);
@ -178,11 +178,11 @@ describe("Manage Groups", () => {
// App operations
cy.createApp(data.appName);
cy.verifyToastMessage(
commonSelectors.toastMessage,
commonText.appCreatedToast,
false
);
// cy.verifyToastMessage(
// commonSelectors.toastMessage,
// commonText.appCreatedToast,
// false
// );
cy.backToApps();
cy.deleteApp(data.appName);

View file

@ -196,10 +196,10 @@ describe("Manage Groups", () => {
// App operations
cy.createApp(data.appName);
cy.verifyToastMessage(
commonSelectors.toastMessage,
commonText.appCreatedToast
);
// cy.verifyToastMessage(
// commonSelectors.toastMessage,
// commonText.appCreatedToast
// );
cy.backToApps();
cy.wait(2500);

View file

@ -0,0 +1,662 @@
import { fake } from "Fixtures/fake";
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
import { commonEeText, ssoEeText } from "Texts/eeCommon";
import { commonEeSelectors, multiEnvSelector } from "Selectors/eeCommon";
import {
verifyPromoteModalUI,
verifyTooltipDisabled,
} from "Support/utils/platform/eeCommon";
import { dataSourceSelector } from "Selectors/dataSource";
import {
navigateToAppEditor,
pinInspector,
verifyTooltip,
} from "Support/utils/common";
import { addQuery, selectDatasource } from "Support/utils/dataSource";
import {
appPromote,
createNewVersion,
selectVersion,
selectEnv,
} from "Support/utils/platform/multiEnv";
import { appVersionSelectors } from "Selectors/exportImport";
import { editAndVerifyWidgetName } from "Support/utils/commonWidget";
import { deleteVersionAndVerify } from "Support/utils/version";
import { deleteVersionText } from "Texts/version";
describe("Multi env", () => {
const data = {};
data.appName = `${fake.companyName} App`;
data.ds = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", "");
data.constName = fake.firstName.toLowerCase().replaceAll("[^A-Za-z]", "");
const slug = data.appName.toLowerCase().replace(/\s+/g, "-");
let currentVersion = "";
let newVersion = [];
let versionFrom = "";
beforeEach(() => {
cy.apiLogin();
cy.viewport(1800, 1800);
cy.skipWalkthrough();
});
it.only("Verify the datasource configuration and data on each env", () => {
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
data.ds,
"restapi",
[
{ key: "url", value: "" },
{ key: "auth_type", value: "none" },
{ key: "grant_type", value: "authorization_code" },
{ key: "add_token_to", value: "header" },
{ key: "header_prefix", value: "Bearer " },
{ key: "access_token_url", value: "" },
{ key: "client_ide", value: "" },
{ key: "client_secret", value: "", encrypted: true },
{ key: "scopes", value: "read, write" },
{ key: "username", value: "", encrypted: false },
{ key: "password", value: "", encrypted: true },
{ key: "bearer_token", value: "", encrypted: true },
{ key: "auth_url", value: "" },
{ key: "client_auth", value: "header" },
{ key: "headers", value: [["", ""]] },
{ key: "custom_query_params", value: [["", ""]], encrypted: false },
{ key: "custom_auth_params", value: [["", ""]] },
{
key: "access_token_custom_headers",
value: [["", ""]],
encrypted: false,
},
{ key: "multiple_auth_enabled", value: false, encrypted: false },
{ key: "ssl_certificate", value: "none", encrypted: false },
]
);
cy.apiCreateApp(data.appName);
cy.visit("/my-workspace");
cy.get(commonSelectors.globalDataSourceIcon).click();
selectDatasource(data.ds);
cy.get('[data-cy="development-label"]').click();
cy.clearAndType(
'[data-cy="base-url-text-field"]',
"https://reqres.in/api/users?page=1"
);
cy.get(dataSourceSelector.buttonSave).click();
cy.wait(2000);
cy.get(commonSelectors.dashboardIcon).click();
cy.openApp();
// cy.waitForAppLoad();
cy.wait(2000);
cy.get(`[data-cy="${data.ds}-add-query-card"] > .text-truncate`).click();
cy.wait(1000);
cy.get(dataSourceSelector.queryCreateAndRunButton).click();
cy.get('[data-cy="query-tab-settings"]').click();
cy.get(':nth-child(1) > .custom-toggle-switch > .switch > .slider').click();
cy.waitForAutoSave();
cy.dragAndDropWidget("Text Input", 550, 650);
editAndVerifyWidgetName(data.constName, []);
cy.waitForAutoSave();
cy.get(
'[data-cy="default-value-input-field"]'
).clearAndTypeOnCodeMirror(`{{queries.restapi1.data.data[0].email`);
cy.wait(1000);
cy.forceClickOnCanvas();
cy.waitForAutoSave();
cy.get(dataSourceSelector.queryCreateAndRunButton).click();
cy.get(
commonWidgetSelector.draggableWidget(data.constName)
).verifyVisibleElement("have.value", "george.bluth@reqres.in");
pinInspector();
cy.get(commonWidgetSelector.sidebarinspector).click();
cy.get(commonWidgetSelector.inspectorNodeComponents).click();
cy.get(commonWidgetSelector.nodeComponent(data.constName)).click();
cy.get('[data-cy="inspector-node-value"] > .mx-2').verifyVisibleElement(
"have.text",
`"george.bluth@reqres.in"`
);
cy.get('[style="height: 13px; width: 13px;"] > img').should("exist");
cy.get('[data-cy="inspector-node-globals"] > .node-key').click();
cy.get('[data-cy="inspector-node-environment"] > .node-key').click();
cy.get('[data-cy="inspector-node-name"] > .mx-2').verifyVisibleElement(
"have.text",
`"development"`
);
cy.openInCurrentTab(commonWidgetSelector.previewButton);
cy.wait(4000);
cy.get(
commonWidgetSelector.draggableWidget(data.constName)
).verifyVisibleElement("have.value", "george.bluth@reqres.in");
cy.go("back");
cy.waitForAppLoad();
cy.wait(3000);
cy.get(commonEeSelectors.promoteButton).click();
cy.get(commonEeSelectors.promoteButton).eq(1).click();
cy.waitForAppLoad();
cy.wait(3000);
cy.get(dataSourceSelector.queryCreateAndRunButton, {
timeout: 20000,
}).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
"Query could not be completed"
);
cy.backToApps();
cy.get(commonSelectors.globalDataSourceIcon).click();
selectDatasource(data.ds);
cy.get('[data-cy="staging-label"]').click();
cy.clearAndType(
'[data-cy="base-url-text-field"]',
"https://reqres.in/api/users?page=2"
);
cy.get(dataSourceSelector.buttonSave).click();
cy.wait(2000);
cy.get(commonSelectors.dashboardIcon).click();
navigateToAppEditor(data.appName);
cy.get(dataSourceSelector.queryCreateAndRunButton).click();
cy.get(
commonWidgetSelector.draggableWidget(data.constName)
).verifyVisibleElement("have.value", "michael.lawson@reqres.in");
cy.get(commonWidgetSelector.sidebarinspector).click();
cy.get(commonWidgetSelector.inspectorNodeComponents).click();
cy.get(commonWidgetSelector.nodeComponent(data.constName)).click();
cy.get('[data-cy="inspector-node-value"] > .mx-2').verifyVisibleElement(
"have.text",
`"michael.lawson@reqres.in"`
);
cy.get('[style="height: 13px; width: 13px;"] > img').should("not.exist");
cy.get('[data-cy="inspector-node-globals"] > .node-key').click();
cy.get('[data-cy="inspector-node-environment"] > .node-key').click();
cy.get('[data-cy="inspector-node-name"] > .mx-2').verifyVisibleElement(
"have.text",
`"staging"`
);
cy.openInCurrentTab(commonWidgetSelector.previewButton);
cy.wait(4000);
cy.get(
commonWidgetSelector.draggableWidget(data.constName)
).verifyVisibleElement("have.value", "michael.lawson@reqres.in");
cy.go("back");
cy.waitForAppLoad();
cy.wait(3000);
cy.get(commonEeSelectors.promoteButton).click();
cy.get(commonEeSelectors.promoteButton).eq(1).click();
cy.waitForAppLoad();
cy.wait(3000);
cy.get(dataSourceSelector.queryCreateAndRunButton, {
timeout: 20000,
}).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
"Query could not be completed"
);
cy.backToApps();
cy.get(commonSelectors.globalDataSourceIcon).click();
selectDatasource(data.ds);
cy.get('[data-cy="production-label"]').click();
cy.clearAndType(
'[data-cy="base-url-text-field"]',
"https://reqres.in/api/users?page=1"
);
cy.get(dataSourceSelector.buttonSave).click();
cy.wait(2000);
cy.get(commonSelectors.dashboardIcon).click();
navigateToAppEditor(data.appName);
cy.get(dataSourceSelector.queryCreateAndRunButton).click();
cy.get(
commonWidgetSelector.draggableWidget(data.constName)
).verifyVisibleElement("have.value", "george.bluth@reqres.in");
cy.get(commonWidgetSelector.sidebarinspector).click();
cy.get(commonWidgetSelector.inspectorNodeComponents).click();
cy.get(commonWidgetSelector.nodeComponent(data.constName)).click();
cy.get('[data-cy="inspector-node-value"] > .mx-2').verifyVisibleElement(
"have.text",
`"george.bluth@reqres.in"`
);
cy.get('[style="height: 13px; width: 13px;"] > img').should("not.exist");
cy.get('[data-cy="inspector-node-globals"] > .node-key').click();
cy.get('[data-cy="inspector-node-environment"] > .node-key').click();
cy.get('[data-cy="inspector-node-name"] > .mx-2').verifyVisibleElement(
"have.text",
`"production"`
);
cy.openInCurrentTab(commonWidgetSelector.previewButton);
cy.wait(4000);
cy.get(
commonWidgetSelector.draggableWidget(data.constName)
).verifyVisibleElement("have.value", "george.bluth@reqres.in");
cy.go("back");
cy.waitForAppLoad();
cy.wait(3000);
cy.get(commonSelectors.releaseButton).click();
cy.get(commonSelectors.yesButton).click();
cy.verifyToastMessage(commonSelectors.toastMessage, "Version v1 released");
cy.wait(4000);
cy.get(commonWidgetSelector.shareAppButton).click();
cy.clearAndType(commonWidgetSelector.appNameSlugInput, `${slug}`);
cy.wait(2000);
cy.get(commonWidgetSelector.modalCloseButton).click();
cy.visit(`/applications/${slug}`);
cy.get(
commonWidgetSelector.draggableWidget(data.constName)
).verifyVisibleElement("have.value", "george.bluth@reqres.in");
});
it("should verify edit privilages of a promoted version", () => {
data.appName = `${fake.companyName} App`;
cy.apiCreateApp(data.appName);
cy.openApp();
cy.waitForAppLoad();
cy.dragAndDropWidget("Text", 550, 650);
appPromote("development", "production");
createNewVersion(
(currentVersion = "v1"),
(newVersion = ["v2"]),
(versionFrom = "v1")
);
appPromote("development", "release");
createNewVersion(
(currentVersion = "v2"),
(newVersion = ["v3"]),
(versionFrom = "v2")
);
appPromote("development", "staging");
selectVersion((currentVersion = "v3"), (newVersion = ["v1"]));
cy.get(commonSelectors.warningText).eq(0).verifyVisibleElement(
"have.text",
"App cannot be edited after promotion. Please create a new version from Development to make any changes."
);
cy.forceClickOnCanvas();
cy.get(".datasource-picker").should("have.class", "disabled");
cy.get(commonEeSelectors.AddQueryButton).should("be.disabled");
cy.get(".components-container").should("have.class", "disabled");
cy.wait(1000);
selectEnv("development");
cy.get(commonSelectors.warningText).eq(0).verifyVisibleElement(
"have.text",
"App cannot be edited after promotion. Please create a new version from Development to make any changes."
);
cy.get(".datasource-picker").should("have.class", "disabled");
cy.get(commonEeSelectors.AddQueryButton).should("be.disabled");
cy.get(".components-container").should("have.class", "disabled");
selectVersion((currentVersion = "v1"), (newVersion = ["v2"]));
cy.get(commonSelectors.warningText).eq(0).verifyVisibleElement(
"have.text",
"This version of the app is released. Please create a new version in development to make any changes."
);
cy.get(".datasource-picker").should("have.class", "disabled");
cy.get(commonEeSelectors.AddQueryButton).should("be.disabled");
cy.get(".components-container").should("have.class", "disabled");
cy.wait(1000);
selectEnv("staging");
cy.get(commonSelectors.warningText).eq(0).verifyVisibleElement(
"have.text",
"This version of the app is released. Please create a new version in development to make any changes."
);
cy.get(".datasource-picker").should("have.class", "disabled");
cy.get(commonEeSelectors.AddQueryButton).should("be.disabled");
cy.get(".components-container").should("have.class", "disabled");
cy.wait(1000);
selectEnv("production");
cy.get(commonSelectors.warningText).eq(0).verifyVisibleElement(
"have.text",
"This version of the app is released. Please create a new version in development to make any changes."
);
cy.get(".datasource-picker").should("have.class", "disabled");
cy.get(commonEeSelectors.AddQueryButton).should("be.disabled");
cy.get(".components-container").should("have.class", "disabled");
cy.get(commonSelectors.releaseButton).should("be.disabled");
});
it("Should verify last exisiting version", () => {
data.appName = `${fake.companyName} App`;
cy.apiCreateApp(data.appName);
cy.openApp();
cy.waitForAppLoad();
cy.dragAndDropWidget("Text", 550, 650);
appPromote("development", "staging");
createNewVersion(
(currentVersion = "v1"),
(newVersion = ["v2"]),
(versionFrom = "v1")
);
selectVersion((currentVersion = "v2"), (newVersion = ["v1"]));
cy.wait(1000);
selectEnv("staging");
cy.get(appVersionSelectors.currentVersionField(newVersion[0]))
.should("be.visible")
.and("have.text", "v1");
appPromote("staging", "production");
cy.wait(3000)
deleteVersionAndVerify(
(currentVersion = "v1"),
deleteVersionText.deleteToastMessage((currentVersion = "v1"))
);
cy.wait(2000);
cy.get('[data-cy="list-current-env-name"]').click();
verifyTooltip(
'[data-cy="env-name-dropdown"]:eq(1)',
"There are no versions in this environment"
);
verifyTooltip(
'[data-cy="env-name-dropdown"]:eq(2)',
"There are no versions in this environment"
);
});
it("Should verify version deletion", () => {
data.appName = `${fake.companyName} App`;
cy.apiCreateApp(data.appName);
cy.openApp();
cy.waitForAppLoad();
cy.dragAndDropWidget("Text", 550, 650);
appPromote("development", "staging");
createNewVersion(
(currentVersion = "v1"),
(newVersion = ["v2"]),
(versionFrom = "v1")
);
appPromote("development", "staging");
createNewVersion(
(currentVersion = "v2"),
(newVersion = ["v3"]),
(versionFrom = "v2")
);
appPromote("development", "production");
selectEnv("staging");
selectVersion((currentVersion = "v3"), (newVersion = ["v2"]));
deleteVersionAndVerify(
(currentVersion = "v2"),
deleteVersionText.deleteToastMessage((currentVersion = "v2"))
);
cy.get('[data-cy="v3-current-version-text"]')
.should("be.visible")
.and("have.text", "v3");
cy.get('[data-cy="list-current-env-name"]').should(
"have.text",
"Staging"
);
})
it("Verify the multi env components UI", () => {
data.appName = `${fake.companyName} App`;
cy.apiCreateApp(data.appName);
cy.openApp();
cy.waitForAppLoad();
cy.dragAndDropWidget("Text", 550, 650);
cy.get(multiEnvSelector.envContainer).should("be.visible");
cy.get(multiEnvSelector.currentEnvName)
.verifyVisibleElement("have.text", "Development")
.click();
cy.get(multiEnvSelector.envArrow).should("be.visible");
cy.get(multiEnvSelector.selectedEnvName).verifyVisibleElement(
"have.text",
" Development"
);
cy.get(multiEnvSelector.envNameList)
.eq(0)
.verifyVisibleElement("have.text", "Development");
cy.get(multiEnvSelector.envNameList)
.eq(1)
.verifyVisibleElement("have.text", "Staging");
cy.get(multiEnvSelector.envNameList)
.eq(2)
.verifyVisibleElement("have.text", "Production");
verifyTooltip(
'[data-cy="env-name-dropdown"]:eq(1)',
"There are no versions in this environment"
);
verifyTooltip(
'[data-cy="env-name-dropdown"]:eq(2)',
"There are no versions in this environment"
);
cy.get(multiEnvSelector.appVersionLabel).should("be.visible");
cy.get('[data-cy="v1-current-version-text"]')
.verifyVisibleElement("have.text", "v1")
.click();
cy.get(multiEnvSelector.currentVersion).verifyVisibleElement(
"have.text",
"v1"
);
cy.get(".col-10 > .app-version-name").verifyVisibleElement(
"have.text",
"v1"
);
cy.get(multiEnvSelector.createNewVersionButton).verifyVisibleElement(
"have.text",
"Create new version"
);
verifyPromoteModalUI("v1", "Development", "Staging");
cy.get('[data-cy="env-change-info-text"]').verifyVisibleElement(
"have.text",
"You wont be able to edit this version after promotion. Are you sure you want to continue?"
);
cy.get(commonSelectors.closeButton).click();
cy.get(multiEnvSelector.currentEnvName).verifyVisibleElement(
"have.text",
"Development"
);
cy.get(commonEeSelectors.promoteButton).click();
cy.get(commonSelectors.cancelButton).click();
cy.get(multiEnvSelector.currentEnvName).verifyVisibleElement(
"have.text",
"Development"
);
cy.get(commonEeSelectors.promoteButton).click();
cy.get(commonEeSelectors.promoteButton).eq(1).click();
cy.waitForAppLoad();
cy.wait(3000);
cy.get(commonSelectors.warningText).eq(0).verifyVisibleElement(
"have.text",
"App cannot be edited after promotion. Please create a new version from Development to make any changes."
);
cy.get(multiEnvSelector.envContainer).should("be.visible");
cy.get(multiEnvSelector.currentEnvName)
.verifyVisibleElement("have.text", "Staging")
.click();
cy.get(multiEnvSelector.envArrow).should("be.visible");
cy.get(multiEnvSelector.currentEnvName).verifyVisibleElement(
"have.text",
"Staging"
);
cy.get(multiEnvSelector.envNameList)
.eq(0)
.verifyVisibleElement("have.text", "Development");
cy.get(multiEnvSelector.envNameList)
.eq(1)
.verifyVisibleElement("have.text", "Staging");
cy.get(multiEnvSelector.envNameList)
.eq(2)
.verifyVisibleElement("have.text", "Production");
cy.wait(2000)
verifyTooltip(
'[data-cy="env-name-dropdown"]:eq(2)',
"There are no versions in this environment"
);
cy.get(multiEnvSelector.appVersionLabel).should("be.visible");
cy.get('[data-cy="v1-current-version-text"]')
.verifyVisibleElement("have.text", "v1")
.click();
cy.get(multiEnvSelector.currentVersion).verifyVisibleElement(
"have.text",
"v1"
);
cy.get(".col-10 > .app-version-name").verifyVisibleElement(
"have.text",
"v1"
);
cy.get(multiEnvSelector.createNewVersionButton).verifyVisibleElement(
"have.text",
"Create new version"
);
verifyTooltip(
multiEnvSelector.createNewVersionButton,
"New versions can only be created in development"
);
cy.forceClickOnCanvas();
cy.get(".datasource-picker").should("have.class", "disabled");
cy.get(commonEeSelectors.AddQueryButton).should("be.disabled");
cy.get(".components-container").should("have.class", "disabled");
verifyPromoteModalUI("v1", "Staging", "Production");
cy.get(commonSelectors.closeButton).click();
cy.get(multiEnvSelector.currentEnvName).verifyVisibleElement(
"have.text",
"Staging"
);
cy.get(commonEeSelectors.promoteButton).click();
cy.get(commonSelectors.cancelButton).click();
cy.get(multiEnvSelector.currentEnvName).verifyVisibleElement(
"have.text",
"Staging"
);
cy.get(commonEeSelectors.promoteButton).click();
cy.get(commonEeSelectors.promoteButton).eq(1).click();
cy.waitForAppLoad();
cy.wait(3000);
cy.get(commonSelectors.warningText).eq(0).verifyVisibleElement(
"have.text",
"App cannot be edited after promotion. Please create a new version from Development to make any changes."
);
cy.get(multiEnvSelector.envContainer).should("be.visible");
cy.get(multiEnvSelector.currentEnvName)
.verifyVisibleElement("have.text", "Production")
.click();
cy.get(multiEnvSelector.envArrow).should("be.visible");
cy.get(multiEnvSelector.currentEnvName).verifyVisibleElement(
"have.text",
"Production"
);
cy.get(multiEnvSelector.envNameList)
.eq(0)
.verifyVisibleElement("have.text", "Development");
cy.get(multiEnvSelector.envNameList)
.eq(1)
.verifyVisibleElement("have.text", "Staging");
cy.get(multiEnvSelector.envNameList)
.eq(2)
.verifyVisibleElement("have.text", "Production");
cy.get(multiEnvSelector.appVersionLabel).should("be.visible");
cy.get('[data-cy="v1-current-version-text"]')
.verifyVisibleElement("have.text", "v1")
.click();
cy.get(multiEnvSelector.currentVersion).verifyVisibleElement(
"have.text",
"v1"
);
cy.get(".col-10 > .app-version-name").verifyVisibleElement(
"have.text",
"v1"
);
cy.get(multiEnvSelector.createNewVersionButton).verifyVisibleElement(
"have.text",
"Create new version"
);
cy.get(commonSelectors.releaseButton)
.verifyVisibleElement("have.text", "Release")
.click();
cy.get('[data-cy="modal-title"]').verifyVisibleElement(
"have.text",
"Release Version"
);
cy.get(commonSelectors.closeButton).should("be.visible");
cy.get('[data-cy="confirm-dialogue-box-text"]').verifyVisibleElement(
"have.text",
"Are you sure you want to release this version?"
);
cy.get(commonSelectors.cancelButton).verifyVisibleElement(
"have.text",
"Cancel"
);
cy.get(commonSelectors.yesButton).verifyVisibleElement("have.text", "Yes");
cy.get(commonSelectors.closeButton).click();
cy.get(multiEnvSelector.currentEnvName).verifyVisibleElement(
"have.text",
"Production"
);
cy.get(commonSelectors.releaseButton).click();
cy.get(commonSelectors.cancelButton).click();
cy.get(multiEnvSelector.currentEnvName).verifyVisibleElement(
"have.text",
"Production"
);
cy.get(commonSelectors.releaseButton).click();
cy.get(commonSelectors.yesButton).click();
cy.verifyToastMessage(commonSelectors.toastMessage, "Version v1 released");
cy.wait(500);
cy.get(commonSelectors.warningText).eq(0).verifyVisibleElement(
"have.text",
"This version of the app is released. Please create a new version in development to make any changes."
);
cy.get('[data-cy="v1-current-version-text"]').click();
verifyTooltip(
multiEnvSelector.createNewVersionButton,
"New versions can only be created in development"
);
cy.get(".datasource-picker").should("have.class", "disabled");
cy.get(commonEeSelectors.AddQueryButton).should("be.disabled");
cy.get(".components-container").should("have.class", "disabled");
cy.get(commonSelectors.releaseButton).should("be.disabled");
});
});

View file

@ -1,112 +1,132 @@
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
import { appPromote } from "Support/utils/platform/multiEnv";
const slugValidations = [
{ input: "", error: "App slug can't be empty" },
{ input: "_2#", error: "Special characters are not accepted." },
{ input: "t ", error: "Cannot contain spaces" },
{ input: "T", error: "Only lowercase letters are accepted." },
{ input: "", error: "App slug can't be empty" },
{ input: "_2#", error: "Special characters are not accepted." },
{ input: "t ", error: "Cannot contain spaces" },
{ input: "T", error: "Only lowercase letters are accepted." },
];
export const verifySlugValidations = (inputSelector) => {
slugValidations.forEach(({ input, error }) => {
cy.get(inputSelector).clear();
if (input) cy.clearAndType(inputSelector, input);
cy.wait(500);
cy.get(commonWidgetSelector.appSlugErrorLabel).verifyVisibleElement(
"have.text",
error
);
});
slugValidations.forEach(({ input, error }) => {
cy.get(inputSelector).clear();
if (input) cy.clearAndType(inputSelector, input);
cy.wait(500);
cy.get(commonWidgetSelector.appSlugErrorLabel).verifyVisibleElement(
"have.text",
error
);
});
};
export const verifySuccessfulSlugUpdate = (workspaceId, slug) => {
const host = resolveHost();
cy.get('[data-cy="app-slug-accepted-label"]').verifyVisibleElement(
"have.text",
"Slug accepted!"
);
const host = resolveHost();
cy.get('[data-cy="app-slug-accepted-label"]').verifyVisibleElement(
"have.text",
"Slug accepted!"
);
cy.wait(500);
// cy.get(commonWidgetSelector.appLinkSucessLabel).should('be.visible');
cy.get(commonWidgetSelector.appLinkSucessLabel).should(
"have.text",
"Link updated successfully!"
);
cy.get(commonWidgetSelector.appLinkField).verifyVisibleElement(
"have.text",
`${host}/${workspaceId}/apps/${slug}`
);
cy.wait(500);
// cy.get(commonWidgetSelector.appLinkSucessLabel).should('be.visible');
cy.get(commonWidgetSelector.appLinkSucessLabel).should(
"have.text",
"Link updated successfully!"
);
cy.get(commonWidgetSelector.appLinkField).verifyVisibleElement(
"have.text",
`${host}/${workspaceId}/apps/${slug}`
);
};
export const verifyURLs = (workspaceId, slug, page) => {
const baseUrl = Cypress.config("baseUrl");
const baseUrl = Cypress.config("baseUrl");
cy.url().should(
"eq",
page
? `${baseUrl}/${workspaceId}/apps/${slug}/home`
: `${baseUrl}/${workspaceId}/apps/${slug}`
);
cy.url().should(
"eq",
page
? `${baseUrl}/${workspaceId}/apps/${slug}/home`
: `${baseUrl}/${workspaceId}/apps/${slug}`
);
cy.openInCurrentTab(commonWidgetSelector.previewButton);
cy.openInCurrentTab(commonWidgetSelector.previewButton);
cy.ifEnv("Community", () => {
cy.url().should("eq", `${baseUrl}/applications/${slug}/home?version=v1`);
});
cy.ifEnv("Enterprise", () => {
cy.url().should(
"eq",
`${baseUrl}/applications/${slug}/home?env=production&version=v1`
);
});
cy.visit("/my-workspace");
cy.visitSlug({
actualUrl: `${baseUrl}/applications/${slug}`,
});
cy.url().should("eq", `${baseUrl}/applications/${slug}`);
cy.visit("/my-workspace");
cy.visitSlug({
actualUrl: `${baseUrl}/applications/${slug}`,
});
cy.url().should("eq", `${baseUrl}/applications/${slug}`);
};
export const setUpSlug = (slug) => {
cy.get(commonWidgetSelector.shareAppButton).click();
cy.clearAndType(commonWidgetSelector.appNameSlugInput, slug);
cy.get('[data-cy="app-slug-accepted-label"]')
.should("be.visible")
.and("have.text", "Slug accepted!");
cy.get(commonWidgetSelector.modalCloseButton).click();
cy.get(commonWidgetSelector.shareAppButton).click();
cy.clearAndType(commonWidgetSelector.appNameSlugInput, slug);
cy.get('[data-cy="app-slug-accepted-label"]')
.should("be.visible")
.and("have.text", "Slug accepted!");
cy.get(commonWidgetSelector.modalCloseButton).click();
};
export const setupAppWithSlug = (appName, slug) => {
cy.apiCreateApp(appName);
cy.apiAddComponentToApp(appName, "text1");
cy.apiReleaseApp(appName);
cy.apiAddAppSlug(appName, slug);
cy.apiCreateApp(appName);
cy.apiAddComponentToApp(appName, "text1");
cy.ifEnv("Enterprise", () => {
cy.openApp(
"",
Cypress.env("workspaceId"),
Cypress.env("appId"),
commonWidgetSelector.draggableWidget("text1")
);
appPromote("development", "production");
});
cy.apiReleaseApp(appName);
cy.apiAddAppSlug(appName, slug);
};
export const verifyRestrictedAccess = () => {
cy.get('[data-cy="modal-header"]').should("have.text", "Restricted access");
cy.get('[data-cy="modal-description"]')
.invoke("text")
.then((text) => {
const normalizedText = text.replace(//g, "'");
expect(normalizedText).to.equal(
"You don't have access to this app. Kindly contact admin to know more."
);
});
cy.get('[data-cy="back-to-home-button"]').verifyVisibleElement(
"have.text",
"Back to home page"
);
cy.get('[data-cy="modal-header"]').should("have.text", "Restricted access");
cy.get('[data-cy="modal-description"]')
.invoke("text")
.then((text) => {
const normalizedText = text.replace(//g, "'");
expect(normalizedText).to.equal(
"You don't have access to this app. Kindly contact admin to know more."
);
});
cy.get('[data-cy="back-to-home-button"]').verifyVisibleElement(
"have.text",
"Back to home page"
);
};
export const onboardUserFromAppLink = (
email,
slug,
workspaceName = "My workspace",
isNonExistingUser = true
email,
slug,
workspaceName = "My workspace",
isNonExistingUser = true
) => {
const dbConfig = Cypress.env("app_db");
const dbConfig = Cypress.env("app_db");
const query = isNonExistingUser
? `
const query = isNonExistingUser
? `
SELECT u.invitation_token, o.id AS workspace_id, ou.invitation_token AS organization_token
FROM users u
JOIN organization_users ou ON u.id = ou.user_id
JOIN organizations o ON ou.organization_id = o.id
WHERE u.email = '${email}' AND o.name = '${workspaceName}';
`
: `
: `
SELECT ou.invitation_token, o.id AS workspace_id
FROM users u
JOIN organization_users ou ON u.id = ou.user_id
@ -114,33 +134,33 @@ export const onboardUserFromAppLink = (
WHERE u.email = '${email}' AND o.name = '${workspaceName}';
`;
cy.task("dbConnection", { dbconfig: dbConfig, sql: query }).then((resp) => {
if (!resp.rows || resp.rows.length === 0) {
throw new Error(
`No records found for email: ${email} and workspace: ${workspaceName}`
);
}
cy.task("dbConnection", { dbconfig: dbConfig, sql: query }).then((resp) => {
if (!resp.rows || resp.rows.length === 0) {
throw new Error(
`No records found for email: ${email} and workspace: ${workspaceName}`
);
}
const { invitation_token, workspace_id, organization_token } = resp.rows[0];
const token = isNonExistingUser ? organization_token : invitation_token;
const url = isNonExistingUser
? `${Cypress.config("baseUrl")}/invitations/${invitation_token}/workspaces/${organization_token}?oid=${workspace_id}&redirectTo=%2Fapplications%2F${slug}`
: `${Cypress.config("baseUrl")}/organization-invitations/${token}?oid=${workspace_id}&redirectTo=%2Fapplications%2F${slug}`;
const { invitation_token, workspace_id, organization_token } = resp.rows[0];
const token = isNonExistingUser ? organization_token : invitation_token;
const url = isNonExistingUser
? `${Cypress.config("baseUrl")}/invitations/${invitation_token}/workspaces/${organization_token}?oid=${workspace_id}&redirectTo=%2Fapplications%2F${slug}`
: `${Cypress.config("baseUrl")}/organization-invitations/${token}?oid=${workspace_id}&redirectTo=%2Fapplications%2F${slug}`;
cy.visit(url);
});
cy.visit(url);
});
};
export const resolveHost = () => {
const baseUrl = Cypress.config("baseUrl");
const baseUrl = Cypress.config("baseUrl");
const urlMapping = {
"http://localhost:8082": "http://localhost:8082",
"http://localhost:3000": "http://localhost:3000",
"http://localhost:3000/apps": "http://localhost:3000/apps",
"http://localhost:4001": "http://localhost:3000",
"http://localhost:4001/apps": "http://localhost:3000/apps",
};
const urlMapping = {
"http://localhost:8082": "http://localhost:8082",
"http://localhost:3000": "http://localhost:3000",
"http://localhost:3000/apps": "http://localhost:3000/apps",
"http://localhost:4001": "http://localhost:3000",
"http://localhost:4001/apps": "http://localhost:3000/apps",
};
return urlMapping[baseUrl];
return urlMapping[baseUrl];
};

View file

@ -14,9 +14,21 @@ export const verifyComponent = (widgetName) => {
};
export const verifyComponentinrightpannel = (widgetName) => {
cy.get(commonWidgetSelector.widgetBox(widgetName), {
timeout: 10000,
}).should("be.visible");
cy.get("body")
.then(($body) => {
const isSearchVisible = $body
.find(commonSelectors.searchField)
.is(":visible");
if (!isSearchVisible) {
cy.get('[data-cy="right-sidebar-plus-button"]').click();
}
})
.then(() => {
cy.get(commonWidgetSelector.widgetBox(widgetName), {
timeout: 10000,
}).should("be.visible");
});
};
export const deleteComponentAndVerify = (widgetName) => {

View file

@ -6,6 +6,7 @@ import moment from "moment";
import { dashboardSelector } from "Selectors/dashboard";
import { groupsSelector } from "Selectors/manageGroups";
import { groupsText } from "Texts/manageGroups";
import { appPromote } from "Support/utils/platform/multiEnv";
export const navigateToProfile = () => {
cy.get(commonSelectors.settingsIcon).click();
@ -48,7 +49,7 @@ export const randomDateOrTime = (format = "DD/MM/YYYY") => {
let startDate = new Date(2018, 0, 1);
startDate = new Date(
startDate.getTime() +
Math.random() * (endDate.getTime() - startDate.getTime())
Math.random() * (endDate.getTime() - startDate.getTime())
);
return moment(startDate).format(format);
};
@ -104,7 +105,7 @@ export const viewAppCardOptions = (appName) => {
cy.get(commonSelectors.appCard(appName))
.realHover()
.find(commonSelectors.appCardOptionsButton)
.realHover()
.realHover();
cy.contains("div", appName)
.parent()
.within(() => {
@ -230,6 +231,9 @@ export const navigateToworkspaceConstants = () => {
};
export const releaseApp = () => {
cy.ifEnv("Enterprise", () => {
appPromote("development", "production");
});
cy.get(commonSelectors.releaseButton).click();
cy.get(commonSelectors.yesButton).click();
cy.verifyToastMessage(commonSelectors.toastMessage, "Version v1 released");

View file

@ -92,7 +92,7 @@ export const userSignUp = (fullName, email, workspaceName = "test") => {
cy.visit(invitationLink);
cy.wait(2500);
});
if (Cypress.env("environment") !== "Community") {
if (Cypress.env("environment") == "Cloud") {
cy.clearAndType(
'[data-cy="onboarding-workspace-name-input"]',
workspaceName

View file

@ -0,0 +1,120 @@
import { multiEnvSelector, commonEeSelectors } from "Selectors/eeCommon";
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
import { appVersionSelectors } from "Selectors/exportImport";
import { appVersionText } from "Texts/exportImport";
export const promoteApp = () => {
cy.get(commonEeSelectors.promoteButton).click();
cy.get(commonEeSelectors.promoteButton).eq(1).click();
cy.waitForAppLoad();
cy.wait(3000);
};
export const releaseApp = () => {
cy.get(commonSelectors.releaseButton).click();
cy.get(commonSelectors.yesButton).click();
cy.verifyToastMessage(commonSelectors.toastMessage, "Version v1 released");
cy.wait(500);
};
export const launchApp = () => {
cy.url().then((url) => {
const parts = url.split("/");
const value = parts[parts.length - 1];
cy.log(`Extracted value: ${value}`);
cy.visit(`/applications/${value}`);
cy.wait(3000);
});
};
export const appPromote = (fromEnv, toEnv) => {
const commonActions = () => {
cy.get(commonEeSelectors.promoteButton).click();
cy.get(commonEeSelectors.promoteButton).eq(1).click();
cy.waitForAppLoad();
cy.wait(2000);
};
const transitions = {
development: {
staging: commonActions,
production: () => {
commonActions();
appPromote("staging", "production");
},
release: () => {
commonActions();
commonActions();
cy.get(commonSelectors.releaseButton).click();
cy.get(commonSelectors.yesButton).click();
cy.wait(500);
},
},
staging: {
production: commonActions,
release: () => {
commonActions();
cy.get(commonSelectors.releaseButton).click();
cy.get(commonSelectors.yesButton).click();
cy.wait(500);
},
},
};
const transition = transitions[fromEnv]?.[toEnv];
transition();
};
export const createNewVersion = (value, newVersion = [], version) => {
cy.get('[data-cy="list-current-env-name"]').click();
cy.get(multiEnvSelector.envNameList).eq(0).click();
cy.get(appVersionSelectors.currentVersionField(value)).click();
cy.get(appVersionSelectors.createNewVersionButton).click();
cy.get(appVersionSelectors.createVersionInputField).click();
cy.contains(`[id*="react-select-"]`, version).click();
cy.get(appVersionSelectors.versionNameInputField).click().type(newVersion[0]);
cy.get(appVersionSelectors.createNewVersionButton).click();
cy.waitForAppLoad();
cy.verifyToastMessage(
commonSelectors.toastMessage,
appVersionText.createdToastMessage
);
cy.get(appVersionSelectors.currentVersionField(newVersion[0])).should(
"be.visible"
);
};
export const selectVersion = (value, newVersion = []) => {
cy.get(appVersionSelectors.currentVersionField(value)).click();
cy.get(".react-select__menu-list .app-version-name")
.contains(newVersion[0])
.click();
cy.waitForAppLoad();
};
export const selectEnv = (envName) => {
const envIndex = {
development: 0,
staging: 1,
production: 2,
}[envName];
const isValidEnvName = (envName) => {
return (
envName === "development" ||
envName === "staging" ||
envName === "production"
);
};
if (isValidEnvName(envName)) {
cy.get('[data-cy="list-current-env-name"]').click();
cy.wait(500)
const envSelector = `${multiEnvSelector.envNameList}:eq(${envIndex})`;
cy.get(envSelector).click();
cy.waitForAppLoad();
}
};

View file

@ -11,8 +11,9 @@ export const selectQueryFromLandingPage = (dbName, label) => {
};
export const deleteQuery = (queryName) => {
cy.get(`[data-cy="list-query-${queryName}"]`).realHover();
cy.get(`[data-cy="list-query-${queryName}"]`).click();
cy.get(`[data-cy="delete-query-${queryName}"]`).click();
cy.get('[data-cy="component-inspector-delete-button"]').click()
};
export const query = (action) => {

View file

@ -9,6 +9,7 @@ import {
} from "Selectors/version";
import { deleteVersionText, releasedVersionText } from "Texts/version";
import { verifyComponent } from "Support/utils/basicComponents";
import { appPromote } from "./platform/multiEnv";
export const navigateToCreateNewVersionModal = (value) => {
cy.get(appVersionSelectors.appVersionLabel).click();
@ -121,6 +122,9 @@ export const verifyDuplicateVersion = (newVersion = [], version) => {
};
export const releasedVersionAndVerify = (currentVersion) => {
cy.ifEnv("Enterprise", () => {
appPromote("development", "production");
});
cy.contains("Release").click();
cy.get(confirmVersionModalSelectors.yesButton).click();
@ -146,7 +150,8 @@ export const verifyVersionAfterPreview = (currentVersion) => {
cy.wait(2000);
cy.get('[data-cy^="draggable-widget-table"]').should("be.visible");
cy.url().should("include", `version=${currentVersion}`);
cy.get('[data-cy="viewer-page-logo"]').click();
// cy.get('[data-cy="viewer-page-logo"]').click();
cy.go("back");
cy.wait(8000);
};

View file

@ -1,60 +0,0 @@
# https://docs.tooljet.io/docs/setup/env-vars
TOOLJET_HOST=__required__
LOCKBOX_MASTER_KEY=__required__
SECRET_KEY_BASE=__required__
PG_USER=__required__
PG_HOST=__required__
PG_PASS=__required__
PG_DB=tooljet_prod
ORM_LOGGING=true
NODE_ENV=production
DEPLOYMENT_PLATFORM=ec2
# ToolJet Database
TOOLJET_DB=tooljet_db
TOOLJET_DB_USER=
TOOLJET_DB_HOST=
TOOLJET_DB_PASS=
PGRST_HOST=localhost:3001
PGRST_SERVER_PORT=3001
PGRST_JWT_SECRET=
PGRST_DB_URI=
PGRST_DB_PRE_CONFIG=postgrest.pre_config
# Checks every 24 hours to see if a new version of ToolJet is available
# (Enabled by default. Set 0 to disable)
CHECK_FOR_UPDATES=
# Checks every 24 hours to update app telemetry data to ToolJet hub.
# (Telemetry is enabled by default. Set value to true to disable.)
# DISABLE_APP_TELEMETRY=false
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# EMAIL CONFIGURATION
DEFAULT_FROM_EMAIL=hello@tooljet.io
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_DOMAIN=
SMTP_PORT=
# DISABLE USER SIGNUPS (true or false). Default: true
DISABLE_SIGNUPS=
# OBSERVABILITY
APM_VENDOR=
SENTRY_DNS=
SENTRY_DEBUG=
# FEATURE TOGGLE
COMMENT_FEATURE_ENABLE=
ENABLE_MULTIPLAYER_EDITING=true
#SSO
SSO_DISABLE_SIGNUP=
SSO_RESTRICTED_DOMAIN=
SSO_GOOGLE_OAUTH2_CLIENT_ID=
SSO_GIT_OAUTH2_CLIENT_ID=
SSO_GIT_OAUTH2_CLIENT_SECRET=
SSO_GIT_OAUTH2_HOST=

View file

@ -1,17 +0,0 @@
[Unit]
Description=Nest Server
After=network.target
[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/app
Environment="NODE_ENV=production"
EnvironmentFile=/home/ubuntu/app/.env
RestartSec=1
ExecStart=/usr/bin/npm --prefix /home/ubuntu/app run start:prod
Restart=always
[Install]
WantedBy=multi-user.target

View file

@ -1,16 +0,0 @@
[Unit]
Description=PostgREST Server
After=network.target
[Service]
Type=simple
User=ubuntu
WorkingDirectory=/bin
EnvironmentFile=/home/ubuntu/app/.env
RestartSec=1
ExecStart=/bin/postgrest
Restart=always
[Install]
WantedBy=multi-user.target

View file

@ -1,46 +0,0 @@
#!/bin/bash
if grep __required__ .env
then
echo "Please set the required values within the .env file"
exit 1
fi
export $(grep -v '^#' .env | xargs)
if psql -d postgresql://$PG_USER:$PG_PASS@$PG_HOST/postgres -c 'select now()' > /dev/null 2>&1
then
echo "Successfully pinged the database!";
else
echo "Can't connect to the database. Kindly check the credenials provided in the .env file!"
exit 1
fi
if sudo systemctl start openresty
then
echo "Successfully started reverse proxy!"
else
echo "Failed to start reverse proxy"
exit 1
fi
if $ENABLE_TOOLJET_DB == "true"
then
if sudo systemctl start postgrest
then
echo "Successfully started PostgREST server!"
else
echo "Failed to start PostgREST server"
exit 1
fi
fi
TOOLJET_EDTION=ce npm --prefix server run db:setup:prod
if sudo systemctl start nest
then
echo "The app will be served at ${TOOLJET_HOST}"
else
echo "Failed to start the server!"
exit 1
fi

View file

@ -1,83 +0,0 @@
#!/bin/bash
set -e
# Setup prerequisite dependencies
sudo apt-get update
sudo apt-get -y install --no-install-recommends wget gnupg ca-certificates apt-utils git curl postgresql-client
curl https://raw.githubusercontent.com/creationix/nvm/master/install.sh | bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
nvm install 22.15.1
sudo ln -s "$(which node)" /usr/bin/node
sudo ln -s "$(which npm)" /usr/bin/npm
sudo npm i -g npm@10.9.2
# Setup openresty
wget -O - https://openresty.org/package/pubkey.gpg | sudo apt-key add -
echo "deb http://openresty.org/package/ubuntu bionic main" > openresty.list
sudo mv openresty.list /etc/apt/sources.list.d/
sudo apt-get update
sudo apt-get -y install --no-install-recommends openresty
sudo apt-get install -y curl g++ gcc autoconf automake bison libc6-dev \
libffi-dev libgdbm-dev libncurses5-dev libsqlite3-dev libtool \
libyaml-dev make pkg-config sqlite3 zlib1g-dev libgmp-dev \
libreadline-dev libssl-dev libmysqlclient-dev build-essential \
freetds-dev libpq-dev
sudo apt-get install -y luarocks
sudo luarocks install lua-resty-auto-ssl
sudo mkdir /etc/resty-auto-ssl /var/log/openresty /etc/fallback-certs
sudo chown -R www-data:www-data /etc/resty-auto-ssl
# Oracle db client library setup
sudo apt install -y libaio1
curl -o instantclient-basiclite.zip https://download.oracle.com/otn_software/linux/instantclient/instantclient-basiclite-linuxx64.zip -SL && \
curl -o instantclient-basiclite-11.zip https://tooljet-plugins-production.s3.us-east-2.amazonaws.com/marketplace-assets/oracledb/instantclients/instantclient-basiclite-linux.x64-11.2.0.4.0.zip -SL && \
unzip instantclient-basiclite.zip && \
unzip instantclient-basiclite-11.zip && \
sudo mkdir -p /usr/lib/instantclient && sudo mv instantclient*/ /usr/lib/instantclient && \
rm instantclient-basiclite.zip && \
rm instantclient-basiclite-11.zip && \
echo /usr/lib/instantclient/* | sudo tee /etc/ld.so.conf.d/oracle-instantclient.conf > /dev/null && sudo ldconfig
# Set the Instant Client library paths
export LD_LIBRARY_PATH="/usr/lib/instantclient/instantclient_11_2:/usr/lib/instantclient/instantclient_21_10${LD_LIBRARY_PATH}"
# Gen fallback certs
sudo openssl rand -out /home/ubuntu/.rnd -hex 256
sudo chown www-data:www-data /home/ubuntu/.rnd
sudo openssl req -new -newkey rsa:2048 -days 3650 -nodes -x509 \
-subj '/CN=sni-support-required-for-valid-ssl' \
-keyout /etc/fallback-certs/resty-auto-ssl-fallback.key \
-out /etc/fallback-certs/resty-auto-ssl-fallback.crt
# Setup nginx config
export SERVER_HOST="${SERVER_HOST:=localhost}"
export SERVER_USER="${SERVER_USER:=www-data}"
VARS_TO_SUBSTITUTE='$SERVER_HOST:$SERVER_USER'
envsubst "${VARS_TO_SUBSTITUTE}" < /tmp/nginx.conf > /tmp/nginx-substituted.conf
sudo cp /tmp/nginx-substituted.conf /usr/local/openresty/nginx/conf/nginx.conf
# Download and setup postgrest binary
curl -OL https://github.com/PostgREST/postgrest/releases/download/v12.2.0/postgrest-v12.2.0-linux-static-x64.tar.xz
tar xJf postgrest-v12.2.0-linux-static-x64.tar.xz
sudo mv ./postgrest /bin/postgrest
sudo rm postgrest-v12.2.0-linux-static-x64.tar.xz
# Setup app and postgrest as systemd service
sudo cp /tmp/nest.service /lib/systemd/system/nest.service
sudo cp /tmp/postgrest.service /lib/systemd/system/postgrest.service
# Setup app directory
mkdir -p ~/app
git clone -b main https://github.com/ToolJet/ToolJet.git ~/app && cd ~/app
mv /tmp/.env ~/app/.env
mv /tmp/setup_app ~/app/setup_app
sudo chmod +x ~/app/setup_app
npm install -g npm@10.9.2
# Building ToolJet app
npm install -g @nestjs/cli
TOOLJET_EDTION=ce npm run build

View file

@ -1,63 +0,0 @@
packer {
required_plugins {
amazon = {
version = ">= 0.0.1"
source = "github.com/hashicorp/amazon"
}
}
}
source "amazon-ebs" "ubuntu" {
ami_name = "${var.ami_name}"
instance_type = "${var.instance_type}"
region = "${var.ami_region}"
ami_regions = "${var.ami_regions}"
ami_groups = "${var.ami_groups}"
source_ami_filter {
filters = {
name = "ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"
root-device-type = "ebs"
virtualization-type = "hvm"
}
most_recent = true
owners = ["099720109477"]
}
ssh_username = "ubuntu"
ssh_clear_authorized_keys = "true"
}
build {
sources = [
"source.amazon-ebs.ubuntu"
]
provisioner "file" {
source = "nest.service"
destination = "/tmp/nest.service"
}
provisioner "file" {
source = "../../frontend/config/nginx.conf.template"
destination = "/tmp/nginx.conf"
}
provisioner "file" {
source = ".env"
destination = "/tmp/.env"
}
provisioner "file" {
source = "setup_app"
destination = "/tmp/setup_app"
}
provisioner "file" {
source = "postgrest.service"
destination = "/tmp/postgrest.service"
}
provisioner "shell" {
script = "setup_machine.sh"
}
}

View file

@ -1,23 +0,0 @@
variable "ami_name" {
type = string
}
variable "instance_type" {
type = string
default = "t2.medium"
}
variable "ami_region" {
type = string
default = "us-west-1"
}
variable "ami_groups" {
type = list(string)
default = ["all"]
}
variable "ami_regions" {
type = list(string)
default = ["us-west-1", "us-east-1", "us-east-2", "eu-west-2", "eu-central-1", "ap-northeast-1", "ap-southeast-1","ap-northeast-3", "ap-south-1", "ap-northeast-2", "ap-southeast-2", "ca-central-1", "eu-west-1", "eu-north-1", "sa-east-1", "ap-east-1"]
}

View file

@ -161,6 +161,21 @@ else
exit 1
fi
if [[ "$WORKFLOW_WORKER" == "true" ]]; then
echo "WORKER is true. Running the worker..."
npm run worker:prod &
else
echo "WORKER is not true. Skipping the worker execution."
fi
if sudo systemctl start neo4j && sudo systemctl enable neo4j
then
echo "Successfully started Neo4j!"
else
echo "Failed to start and enable Neo4j"
exit 1
fi
TOOLJET_EDTION=ee npm --prefix server run db:setup:prod
if sudo -E systemctl start nest
@ -172,4 +187,4 @@ else
fi
sudo systemctl restart nest
sudo -E systemctl restart postgrest
sudo -E systemctl restart postgrest

View file

@ -78,6 +78,28 @@ sudo cp /tmp/redis-server.service /lib/systemd/system/redis-server.service
# Start and enable Redis service
sudo systemctl daemon-reload
# Setup Neo4j with APOC plugin
wget -O - https://debian.neo4j.com/neotechnology.gpg.key | sudo apt-key add -
echo "deb https://debian.neo4j.com stable 5" | sudo tee /etc/apt/sources.list.d/neo4j.list
sudo apt-get update
sudo apt-get install -y neo4j=1:5.26.6
sudo apt-mark hold neo4j
# Setup APOC plugin
sudo mkdir -p /var/lib/neo4j/plugins
sudo wget -P /var/lib/neo4j/plugins https://github.com/neo4j/apoc/releases/download/5.26.6/apoc-5.26.6-core.jar
# Update Neo4j config
echo "dbms.security.procedures.unrestricted=apoc.*" | sudo tee -a /etc/neo4j/neo4j.conf
echo "dbms.security.procedures.allowlist=apoc.*,algo.*,gds.*" | sudo tee -a /etc/neo4j/neo4j.conf
echo "dbms.directories.plugins=/var/lib/neo4j/plugins" | sudo tee -a /etc/neo4j/neo4j.conf
echo "dbms.security.auth_enabled=true" | sudo tee -a /etc/neo4j/neo4j.conf
# Clean up APT cache
sudo apt-get clean
sudo rm -rf /var/lib/apt/lists/*
# Setup app directory
mkdir -p ~/app
@ -96,4 +118,5 @@ npm install -g npm@10.9.2
# Building ToolJet app
npm install -g @nestjs/cli
TOOLJET_EDTION=ee npm run build
export NODE_OPTIONS='--max-old-space-size=8000'
TOOLJET_EDTION=ee npm run build

View file

@ -16,7 +16,7 @@ source "amazon-ebs" "ubuntu" {
source_ami_filter {
filters = {
name = "ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"
name = "ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"
root-device-type = "ebs"
virtualization-type = "hvm"
}
@ -30,7 +30,7 @@ source "amazon-ebs" "ubuntu" {
launch_block_device_mappings {
device_name = "/dev/sda1"
volume_size = 10
volume_size = 30
delete_on_termination = true
}
@ -47,7 +47,7 @@ build {
}
provisioner "file" {
source = "../../frontend/config/nginx.conf.template"
source = "../../../frontend/config/nginx.conf.template"
destination = "/tmp/nginx.conf"
}

View file

@ -4,7 +4,7 @@ variable "ami_name" {
variable "instance_type" {
type = string
default = "t2.medium"
default = "t2.large"
}
variable "ami_region" {

0
docker/cloud/cloud-entrypoint.sh Normal file → Executable file
View file

View file

@ -1,14 +1,40 @@
FROM node:18.18.2-buster AS builder
FROM node:22.15.1 AS builder
# Fix for JS heap limit allocation issue
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN npm i -g npm@9.8.1
RUN npm i -g npm@10.9.2
RUN mkdir -p /app
# RUN npm cache clean --force
RUN npm cache clean --force
WORKDIR /app
# Set GitHub token and branch as build arguments
ARG CUSTOM_GITHUB_TOKEN
ARG BRANCH_NAME
# Clone and checkout the frontend repository
RUN git config --global url."https://x-access-token:${CUSTOM_GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/"
RUN git config --global http.version HTTP/1.1
RUN git config --global http.postBuffer 524288000
RUN git clone https://github.com/ToolJet/ToolJet.git .
# The branch name needs to be changed the branch with modularisation in CE repo
RUN git checkout ${BRANCH_NAME}
RUN git submodule update --init --recursive
# Checkout the same branch in submodules if it exists, otherwise stay on default branch
RUN git submodule foreach " \
if git show-ref --verify --quiet refs/heads/${BRANCH_NAME} || \
git ls-remote --exit-code --heads origin ${BRANCH_NAME}; then \
git checkout ${BRANCH_NAME}; \
else \
echo 'Branch ${BRANCH_NAME} not found in submodule \$name, falling back to main'; \
git checkout main; \
fi"
# Scripts for building
COPY ./package.json ./package.json
@ -19,6 +45,8 @@ COPY ./plugins/ ./plugins/
RUN NODE_ENV=production npm --prefix plugins run build
RUN npm --prefix plugins prune --production
ENV TOOLJET_EDITION=cloud
# Build frontend
COPY ./frontend/package.json ./frontend/package-lock.json ./frontend/
RUN npm --prefix frontend install
@ -26,32 +54,34 @@ COPY ./frontend/ ./frontend/
RUN npm --prefix frontend run build --production
RUN npm --prefix frontend prune --production
ENV TOOLJET_EDITION=cloud
ENV NODE_ENV=production
# Build server
COPY ./server/package.json ./server/package-lock.json ./server/
RUN npm --prefix server install
COPY ./server/ ./server/
RUN npm install -g @nestjs/cli
RUN npm install -g @nestjs/cli
RUN npm install -g copyfiles
RUN npm --prefix server run build
FROM debian:11
FROM debian:12
RUN apt-get update -yq \
&& apt-get install curl gnupg zip -yq \
&& apt-get install -yq build-essential \
&& apt-get clean -y
RUN curl -O https://nodejs.org/dist/v18.18.2/node-v18.18.2-linux-x64.tar.xz \
&& tar -xf node-v18.18.2-linux-x64.tar.xz \
&& mv node-v18.18.2-linux-x64 /usr/local/lib/nodejs \
RUN curl -O https://nodejs.org/dist/v22.15.1/node-v22.15.1-linux-x64.tar.xz \
&& tar -xf node-v22.15.1-linux-x64.tar.xz \
&& mv node-v22.15.1-linux-x64 /usr/local/lib/nodejs \
&& echo 'export PATH="/usr/local/lib/nodejs/bin:$PATH"' >> /etc/profile.d/nodejs.sh \
&& /bin/bash -c "source /etc/profile.d/nodejs.sh" \
&& rm node-v18.18.2-linux-x64.tar.xz
&& rm node-v22.15.1-linux-x64.tar.xz
ENV PATH=/usr/local/lib/nodejs/bin:$PATH
ENV NODE_ENV=production
ENV TOOLJET_EDITION=cloud
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN apt-get update && \
apt-get install -y postgresql-client freetds-dev libaio1 wget && \
@ -79,21 +109,23 @@ RUN mkdir -p /app
# copy npm scripts
COPY --from=builder /app/package.json ./app/package.json
# copy plugins dependencies
COPY --from=builder /app/plugins/dist ./app/plugins/dist
COPY --from=builder /app/plugins/client.js ./app/plugins/client.js
COPY --from=builder /app/plugins/node_modules ./app/plugins/node_modules
COPY --from=builder /app/plugins/packages/common ./app/plugins/packages/common
COPY --from=builder /app/plugins/package.json ./app/plugins/package.json
# copy frontend build
COPY --from=builder /app/frontend/build ./app/frontend/build
# copy server build
COPY --from=builder /app/server/package.json ./app/server/package.json
COPY --from=builder /app/server/.version ./app/server/.version
COPY --from=builder /app/server/ee/keys ./app/server/ee/keys
COPY --from=builder /app/server/node_modules ./app/server/node_modules
COPY --from=builder /app/server/templates ./app/server/templates
COPY --from=builder /app/server/scripts ./app/server/scripts
COPY --from=builder /app/server/dist ./app/server/dist
COPY --from=builder --chown=appuser:0 /app/server/ee/ai/assets ./app/server/ee/ai/assets
COPY ./docker/cloud/cloud-entrypoint.sh ./app/server/cloud-entrypoint.sh
@ -118,4 +150,4 @@ WORKDIR /app
# Dependencies for scripts outside nestjs
RUN npm install dotenv@10.0.0 joi@17.4.1
ENTRYPOINT ["./server/cloud-entrypoint.sh"]
ENTRYPOINT ["./server/cloud-entrypoint.sh"]

View file

@ -1,16 +1,18 @@
FROM node:18.18.2-buster as builder
FROM node:22.15.1 AS builder
# Fix for JS heap limit allocation issue
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN npm i -g npm@9.8.1
RUN npm i -g npm@10.9.2
RUN npm cache clean --force
RUN npm install -g @nestjs/cli
RUN mkdir -p /app
WORKDIR /app
# Set GitHub token and branch as build arguments
ARG CUSTOM_GITHUB_TOKEN
ARG BRANCH_NAME=main
ARG BRANCH_NAME
# Clone and checkout the frontend repository
RUN git config --global url."https://x-access-token:${CUSTOM_GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/"
@ -25,8 +27,17 @@ RUN git checkout ${BRANCH_NAME}
RUN git submodule update --init --recursive
# Checkout the same branch in submodules if it exists, otherwise stay on default branch
RUN git submodule foreach 'git checkout ${BRANCH_NAME} || true'
RUN git submodule foreach " \
if git show-ref --verify --quiet refs/heads/${BRANCH_NAME} || \
git ls-remote --exit-code --heads origin ${BRANCH_NAME}; then \
git checkout ${BRANCH_NAME}; \
else \
echo 'Branch ${BRANCH_NAME} not found in submodule \$name, falling back to main'; \
git checkout main; \
fi"
# Scripts for building
COPY ./package.json ./package.json
# Building ToolJet plugins
@ -37,28 +48,34 @@ ENV NODE_ENV=production
RUN npm --prefix plugins run build
RUN npm --prefix plugins prune --production
ENV TOOLJET_EDITION=cloud
ENV NODE_ENV=production
# Building ToolJet server
COPY ./server/package.json ./server/package-lock.json ./server/
RUN npm --prefix server install --only=production
RUN npm --prefix server install
COPY ./server/ ./server/
RUN npm install -g @nestjs/cli
RUN npm install -g copyfiles
RUN npm --prefix server run build
FROM debian:11
FROM debian:12
RUN apt-get update -yq \
&& apt-get install curl gnupg zip -yq \
&& apt-get install -yq build-essential \
&& apt-get clean -y
RUN curl -O https://nodejs.org/dist/v18.18.2/node-v18.18.2-linux-x64.tar.xz \
&& tar -xf node-v18.18.2-linux-x64.tar.xz \
&& mv node-v18.18.2-linux-x64 /usr/local/lib/nodejs \
RUN curl -O https://nodejs.org/dist/v22.15.1/node-v22.15.1-linux-x64.tar.xz \
&& tar -xf node-v22.15.1-linux-x64.tar.xz \
&& mv node-v22.15.1-linux-x64 /usr/local/lib/nodejs \
&& echo 'export PATH="/usr/local/lib/nodejs/bin:$PATH"' >> /etc/profile.d/nodejs.sh \
&& /bin/bash -c "source /etc/profile.d/nodejs.sh" \
&& rm node-v18.18.2-linux-x64.tar.xz
&& rm node-v22.15.1-linux-x64.tar.xz
ENV PATH=/usr/local/lib/nodejs/bin:$PATH
ENV NODE_ENV=production
ENV TOOLJET_EDITION=cloud
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN apt-get update && apt-get install -y postgresql-client freetds-dev libaio1 wget
@ -91,10 +108,12 @@ COPY --from=builder /app/plugins/package.json ./app/plugins/package.json
# copy server build
COPY --from=builder /app/server/package.json ./app/server/package.json
COPY --from=builder /app/server/.version ./app/server/.version
COPY --from=builder /app/server/ee/keys ./app/server/ee/keys
COPY --from=builder /app/server/node_modules ./app/server/node_modules
COPY --from=builder /app/server/templates ./app/server/templates
COPY --from=builder /app/server/scripts ./app/server/scripts
COPY --from=builder /app/server/dist ./app/server/dist
COPY --from=builder --chown=appuser:0 /app/server/ee/ai/assets ./app/server/ee/ai/assets
COPY ./docker/cloud/cloud-entrypoint.sh ./app/server/cloud-entrypoint.sh
@ -105,18 +124,25 @@ RUN useradd --create-home --home-dir /home/appuser appuser \
&& chmod u+x /app \
&& chmod -R g=u /app
RUN mkdir -p /home/appuser/.npm/_cacache \
mkdir -p /home/appuser/.npm_cache_tmp \
mkdir -p /home/appuser/.npm/_logs \
&& chown -R appuser:0 /home/appuser/.npm \
&& chmod g+s /home/appuser/.npm_cache_tmp
# Set npm cache directory
ENV npm_config_cache /home/appuser/.npm
RUN npm config set cache /tmp/npm-cache --global
ENV npm_config_cache /tmp/npm-cache
ENV HOME=/home/appuser
# Installing git for simple git commands
RUN apt-get update && apt-get install -y git && apt-get clean
# Switch back to appuser
USER appuser
WORKDIR /app
# Dependencies for scripts outside nestjs
RUN npm install dotenv@10.0.0 joi@17.4.1
RUN npm cache clean --force
ENTRYPOINT ["./server/cloud-entrypoint.sh"]

View file

@ -42,6 +42,20 @@ else
echo "Using external PostgREST at $PGRST_HOST."
fi
# Check WORKLOW_WORKER and skip SETUP_CMD if true
if [ "${WORKFLOW_WORKER}" == "true" ]; then
echo "WORKFLOW_WORKER is set to true. Running worker process."
npm run worker:prod
else
# Determine setup command based on the presence of ./server/dist
if [ -d "./server/dist" ]; then
SETUP_CMD='npm run db:setup:prod'
else
SETUP_CMD='npm run db:setup'
fi
fi
# Neo4j configuration
# ----------------------------------
# Default Neo4j environment values
@ -63,14 +77,14 @@ if [ -n "$NEO4J_AUTH" ]; then
export NEO4J_USERNAME
export NEO4J_PASSWORD
echo "Neo4j authentication configured with username: $NEO4J_USERNAME"
echo "Neo4j authentication configured with username: $NEO4J_USERNAME" >/dev/null 2>&1
else
echo "NEO4J_AUTH not set, using default authentication"
echo "NEO4J_AUTH not set, using default authentication" >/dev/null 2>&1
fi
# Check if Neo4j is already initialized and set password if necessary
if [ "$NEO4J_AUTH" != "none" ] && [ -n "$NEO4J_PASSWORD" ]; then
echo "Setting Neo4j initial password..."
echo "Setting Neo4j initial password..." >/dev/null 2>&1
# Ensure Neo4j is not running before setting the initial password
neo4j stop || true
@ -78,27 +92,27 @@ if [ "$NEO4J_AUTH" != "none" ] && [ -n "$NEO4J_PASSWORD" ]; then
# Set the initial password using the correct command format for Neo4j 5.x
NEO4J_ADMIN_CMD=$(which neo4j-admin)
NEO4J_VERSION=$(neo4j --version | grep -o "[0-9]\+\.[0-9]\+\.[0-9]\+" | head -n 1)
echo "Detected Neo4j version: $NEO4J_VERSION"
echo "Detected Neo4j version: $NEO4J_VERSION" >/dev/null 2>&1
# Use version-specific command format
MAJOR_VERSION=$(echo $NEO4J_VERSION | cut -d. -f1)
if [ "$MAJOR_VERSION" -ge "5" ]; then
# For Neo4j 5.x and higher
echo "Using Neo4j 5.x+ password command format"
echo "Using Neo4j 5.x+ password command format" >/dev/null 2>&1
$NEO4J_ADMIN_CMD dbms set-initial-password "$NEO4J_PASSWORD" --require-password-change=false >/dev/null 2>&1 || {
echo "Warning: Could not set Neo4j password, it may already be set"
echo "Warning: Could not set Neo4j password, it may already be set" >/dev/null 2>&1
}
else
# For Neo4j 4.x and lower
echo "Using Neo4j 4.x password command format" >/dev/null 2>&1
$NEO4J_ADMIN_CMD set-initial-password "$NEO4J_PASSWORD" >/dev/null 2>&1 || {
echo "Warning: Could not set Neo4j password, it may already be set"
echo "Warning: Could not set Neo4j password, it may already be set" >/dev/null 2>&1
}
fi
fi
# Update Neo4j configuration
echo "Configuring Neo4j..."
echo "Configuring Neo4j..." >/dev/null 2>&1
cat > /etc/neo4j/neo4j.conf << EOF
# Neo4j configuration
dbms.security.auth_enabled=true
@ -124,12 +138,12 @@ echo "Starting Neo4j service..."
neo4j console >/dev/null 2>&1 &
# Add a wait for Neo4j to be ready with more robust checking
echo "Waiting for Neo4j to be ready..."
echo "Waiting for Neo4j to be ready..." >/dev/null 2>&1
NEO4J_READY=false
for i in {1..60}; do
# First try standard status check
if neo4j status >/dev/null 2>&1; then
echo "Neo4j is ready (via status check)"
echo "Neo4j is ready 🚀"
NEO4J_READY=true
break
fi
@ -143,7 +157,7 @@ for i in {1..60}; do
fi
fi
echo "Waiting for Neo4j to start... ($i/60)"
echo "Waiting for Neo4j to start... ($i/60)" >/dev/null 2>&1
sleep 2
done
@ -151,19 +165,6 @@ if [ "$NEO4J_READY" = false ]; then
echo "WARNING: Neo4j may not be fully started yet, but continuing..."
fi
# Check WORKLOW_WORKER and skip SETUP_CMD if true
if [ "${WORKFLOW_WORKER}" == "true" ]; then
echo "WORKFLOW_WORKER is set to true. Running worker process."
npm run worker:prod
else
# Determine setup command based on the presence of ./server/dist
if [ -d "./server/dist" ]; then
SETUP_CMD='npm run db:setup:prod'
else
SETUP_CMD='npm run db:setup'
fi
fi
# Wait for PostgreSQL connection
if [ -z "$DATABASE_URL" ]; then
./server/scripts/wait-for-it.sh $PG_HOST:${PG_PORT:-5432} --strict --timeout=300 -- echo "PostgreSQL is up"

View file

@ -3,15 +3,14 @@ FROM node:22.15.1 AS builder
# Fix for JS heap limit allocation issue
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN npm i -g npm@10.9.2
RUN mkdir -p /app
RUN npm cache clean --force
RUN npm i -g npm@10.9.2 && npm cache clean --force
RUN mkdir -p /app
WORKDIR /app
# Set GitHub token and branch as build arguments
ARG CUSTOM_GITHUB_TOKEN
ARG BRANCH_NAME=main
ARG BRANCH_NAME
# Clone and checkout the frontend repository
RUN git config --global url."https://x-access-token:${CUSTOM_GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/"
@ -21,22 +20,28 @@ RUN git config --global http.postBuffer 524288000
RUN git clone https://github.com/ToolJet/ToolJet.git .
# The branch name needs to be changed the branch with modularisation in CE repo
RUN git checkout main
RUN git checkout ${BRANCH_NAME}
RUN git submodule update --init --recursive
# Checkout the same branch in submodules if it exists, otherwise stay on default branch
RUN git submodule foreach 'git checkout ${BRANCH_NAME} || true'
# Checkout the same branch in submodules if it exists, otherwise fallback to main
RUN git submodule foreach " \
if git show-ref --verify --quiet refs/heads/${BRANCH_NAME} || \
git ls-remote --exit-code --heads origin ${BRANCH_NAME}; then \
git checkout ${BRANCH_NAME}; \
else \
echo 'Branch ${BRANCH_NAME} not found in submodule \$name, falling back to main'; \
git checkout main; \
fi"
# Scripts for building
COPY ./package.json ./package.json
# Build plugins
COPY ./plugins/package.json ./plugins/package-lock.json ./plugins/
RUN npm --prefix plugins install
RUN npm --prefix plugins ci --omit=dev
COPY ./plugins/ ./plugins/
RUN NODE_ENV=production npm --prefix plugins run build
RUN npm --prefix plugins prune --production
RUN NODE_ENV=production npm --prefix plugins run build && npm --prefix plugins prune --omit=dev
ENV TOOLJET_EDITION=ee
@ -44,74 +49,50 @@ ENV TOOLJET_EDITION=ee
COPY ./frontend/package.json ./frontend/package-lock.json ./frontend/
RUN npm --prefix frontend install
COPY ./frontend/ ./frontend/
RUN npm --prefix frontend run build --production
RUN npm --prefix frontend prune --production
RUN npm --prefix frontend run build --production && npm --prefix frontend prune --production
ENV NODE_ENV=production
ENV TOOLJET_EDITION=ee
# Build server
COPY ./server/package.json ./server/package-lock.json ./server/
RUN npm --prefix server install
RUN npm --prefix server ci --omit=dev
COPY ./server/ ./server/
RUN npm install -g @nestjs/cli
RUN npm install -g copyfiles
RUN npm --prefix server run build
RUN npm install -g @nestjs/cli && npm install -g copyfiles
RUN npm --prefix server run build && npm prune --production --prefix server
FROM debian:12
RUN apt-get update -yq \
&& apt-get install curl wget gnupg zip -yq \
&& apt-get install -yq build-essential \
&& apt -y install redis \
&& apt-get clean -y
# Install required dependencies for downloading and extracting files
# Install dependencies for PostgREST, curl, tar, etc.
RUN apt-get update && apt-get install -y \
curl tar xz-utils postgresql postgresql-contrib postgresql-client && \
apt-get clean && rm -rf /var/lib/apt/lists/*
curl ca-certificates tar \
&& rm -rf /var/lib/apt/lists/*
# Install PostgREST from official Docker image
COPY --from=postgrest/postgrest:v12.2.0 /bin/postgrest /bin
ENV POSTGREST_VERSION=v12.2.0
RUN apt-get update && apt-get install -y supervisor
RUN curl -Lo postgrest.tar.xz https://github.com/PostgREST/postgrest/releases/download/${POSTGREST_VERSION}/postgrest-v12.2.0-linux-static-x64.tar.xz && \
tar -xf postgrest.tar.xz && \
mv postgrest /postgrest && \
rm postgrest.tar.xz && \
chmod +x /postgrest
# Create supervisord configuration file
RUN echo "[supervisord]\n" \
"nodaemon=true\n" \
"\n" \
"[program:postgrest]\n" \
"command=/bin/postgrest\n" \
"autostart=true\n" \
"autorestart=true\n" \
"stdout_logfile=/dev/stdout\n" \
"stderr_logfile=/dev/stderr\n" \
"stdout_logfile_maxbytes=0\n" \
"stderr_logfile_maxbytes=0\n" \
"\n" \
"[program:neo4j]\n" \
"command=neo4j console\n" \
"autostart=true\n" \
"autorestart=unexpected\n" \
"startsecs=30\n" \
"startretries=999\n" \
"priority=90\n" \
"exitcodes=0,1,2\n" \
"stopsignal=SIGTERM\n" \
"stopasgroup=true\n" \
"killasgroup=true\n" \
"redirect_stderr=true\n" \
"stdout_logfile=/var/log/neo4j/neo4j.log\n" \
"stdout_logfile_backups=10\n" \
"stderr_capture_maxbytes=20MB\n" \
"\n" | sed 's/ //' > /etc/supervisor/conf.d/supervisord.conf
FROM debian:12-slim
# Create a wrapper for PostgREST to prefix its logs
RUN mv /bin/postgrest /bin/postgrest-original && \
echo '#!/bin/bash\n\
exec /bin/postgrest-original "$@" 2>&1 | sed "s/^/[PostgREST] /"\n\
' > /bin/postgrest && \
chmod +x /bin/postgrest
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
wget \
gnupg \
unzip \
ca-certificates \
xz-utils \
tar \
postgresql-client \
redis \
libaio1 \
git \
freetds-dev \
&& apt-get upgrade -y -o Dpkg::Options::="--force-confold" \
&& apt-get autoremove -y \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
RUN curl -O https://nodejs.org/dist/v22.15.1/node-v22.15.1-linux-x64.tar.xz \
@ -125,53 +106,18 @@ ENV PATH=/usr/local/lib/nodejs/bin:$PATH
ENV NODE_ENV=production
ENV TOOLJET_EDITION=ee
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN apt-get update && \
apt-get install -y postgresql-client freetds-dev libaio1 wget && \
apt-get -o Dpkg::Options::="--force-confold" upgrade -q -y --force-yes && \
apt-get -y autoremove && \
apt-get -y autoclean
# Install Neo4j
# Install Neo4j + APOC
RUN wget -O - https://debian.neo4j.com/neotechnology.gpg.key | apt-key add - && \
echo "deb https://debian.neo4j.com stable 5" > /etc/apt/sources.list.d/neo4j.list && \
apt-get update && \
apt-get install -y neo4j=1:5.26.6 && \
apt-mark hold neo4j && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# Set the necessary Neo4j environment variables
ENV NEO4J_HOME=/opt/neo4j
ENV NEO4J_CONF=/etc/neo4j
ENV NEO4J_DATA=/var/lib/neo4j/data
ENV NEO4J_LOG=/var/log/neo4j
ENV NEO4J_PLUGIN=/var/lib/neo4j/plugins
ENV NEO4J_IMPORT=/var/lib/neo4j/import
# Create the necessary directories for Neo4j
RUN mkdir -p /data/db /data/logs /data/plugins
RUN mkdir -p /opt/neo4j/plugins
# Configure APOC plugin for Neo4j
ENV NEO4J_dbms_active_plugins=apoc
# Download and install APOC plugin for Neo4j 5.x (BEFORE creating user)
RUN mkdir -p /var/lib/neo4j/plugins && \
apt-get update && apt-get install -y neo4j=1:5.26.6 && apt-mark hold neo4j && \
mkdir -p /var/lib/neo4j/plugins && \
wget -P /var/lib/neo4j/plugins https://github.com/neo4j/apoc/releases/download/5.26.6/apoc-5.26.6-core.jar && \
# Try to download extended version
(wget -P /var/lib/neo4j/plugins https://github.com/neo4j/apoc/releases/download/5.26.6/apoc-5.26.6-extended.jar || \
wget -P /var/lib/neo4j/plugins https://neo4j-contrib.github.io/neo4j-apoc-procedures/5.26.6/apoc-5.26.6-extended.jar || \
echo "Extended JAR not available, continuing with core only")
# Configure Neo4j with APOC
RUN echo "dbms.security.procedures.unrestricted=apoc.*" >> /etc/neo4j/neo4j.conf && \
echo "dbms.security.procedures.unrestricted=apoc.*" >> /etc/neo4j/neo4j.conf && \
echo "dbms.security.procedures.allowlist=apoc.*,algo.*,gds.*" >> /etc/neo4j/neo4j.conf && \
echo "dbms.directories.plugins=/var/lib/neo4j/plugins" >> /etc/neo4j/neo4j.conf
# Configure Neo4j to use authentication
RUN if [ -f "/etc/neo4j/neo4j.conf" ]; then \
sed -i '/dbms.security.auth_enabled/d' /etc/neo4j/neo4j.conf && \
echo "dbms.security.auth_enabled=true" >> /etc/neo4j/neo4j.conf; \
fi
echo "dbms.directories.plugins=/var/lib/neo4j/plugins" >> /etc/neo4j/neo4j.conf && \
echo "dbms.security.auth_enabled=true" >> /etc/neo4j/neo4j.conf && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# Install Instantclient Basic Light Oracle and Dependencies
WORKDIR /opt/oracle
@ -186,40 +132,39 @@ RUN wget https://tooljet-plugins-production.s3.us-east-2.amazonaws.com/marketpla
# Set the Instant Client library paths
ENV LD_LIBRARY_PATH="/opt/oracle/instantclient_11_2:/opt/oracle/instantclient_21_10:${LD_LIBRARY_PATH}"
RUN rm -f *.zip *.key && apt-get clean && rm -rf /var/lib/apt/lists/*
WORKDIR /
RUN mkdir -p /app
# copy npm scripts
COPY --from=builder /app/package.json ./app/package.json
# copy plugins dependencies
COPY --from=builder /app/plugins/dist ./app/plugins/dist
COPY --from=builder /app/plugins/client.js ./app/plugins/client.js
COPY --from=builder /app/plugins/node_modules ./app/plugins/node_modules
COPY --from=builder /app/plugins/packages/common ./app/plugins/packages/common
COPY --from=builder /app/plugins/package.json ./app/plugins/package.json
# copy frontend build
COPY --from=builder /app/frontend/build ./app/frontend/build
# copy server build
COPY --from=builder /app/server/package.json ./app/server/package.json
COPY --from=builder /app/server/.version ./app/server/.version
COPY --from=builder /app/server/ee/keys ./app/server/ee/keys
COPY --from=builder /app/server/node_modules ./app/server/node_modules
COPY --from=builder /app/server/templates ./app/server/templates
COPY --from=builder /app/server/scripts ./app/server/scripts
COPY --from=builder /app/server/dist ./app/server/dist
COPY --from=builder /app/server/src/assets ./app/server/src/assets
COPY ./docker/ee/ee-entrypoint.sh ./app/server/ee-entrypoint.sh
RUN useradd --create-home --home-dir /home/appuser appuser
# Define non-sudo user
RUN useradd --create-home --home-dir /home/appuser appuser \
&& chown -R appuser:0 /app \
&& chown -R appuser:0 /home \
&& chmod u+x /app \
&& chmod u+x /home \
&& chmod -R g=u /app \
&& chmod -R g=u /home
# Use the PostgREST binary from the builder stage
COPY --from=builder --chown=appuser:0 /postgrest /usr/local/bin/postgrest
RUN mv /usr/local/bin/postgrest /usr/local/bin/postgrest-original && \
echo '#!/bin/bash\nexec /usr/local/bin/postgrest-original "$@" 2>&1 | sed "s/^/[PostgREST] /"' > /usr/local/bin/postgrest && \
chmod +x /usr/local/bin/postgrest
# Copy application with ownership set directly to avoid chown -R
COPY --from=builder --chown=appuser:0 /app/package.json ./app/package.json
COPY --from=builder --chown=appuser:0 /app/plugins/dist ./app/plugins/dist
COPY --from=builder --chown=appuser:0 /app/plugins/client.js ./app/plugins/client.js
COPY --from=builder --chown=appuser:0 /app/plugins/node_modules ./app/plugins/node_modules
COPY --from=builder --chown=appuser:0 /app/plugins/packages/common ./app/plugins/packages/common
COPY --from=builder --chown=appuser:0 /app/plugins/package.json ./app/plugins/package.json
COPY --from=builder --chown=appuser:0 /app/frontend/build ./app/frontend/build
COPY --from=builder --chown=appuser:0 /app/server/package.json ./app/server/package.json
COPY --from=builder --chown=appuser:0 /app/server/.version ./app/server/.version
COPY --from=builder --chown=appuser:0 /app/server/ee/keys ./app/server/ee/keys
COPY --from=builder --chown=appuser:0 /app/server/node_modules ./app/server/node_modules
COPY --from=builder --chown=appuser:0 /app/server/templates ./app/server/templates
COPY --from=builder --chown=appuser:0 /app/server/scripts ./app/server/scripts
COPY --from=builder --chown=appuser:0 /app/server/dist ./app/server/dist
COPY --from=builder --chown=appuser:0 /app/server/ee/ai/assets ./app/server/ee/ai/assets
COPY ./docker/ee/ee-entrypoint.sh ./app/server/ee-entrypoint.sh
RUN mkdir -p /var/lib/neo4j/data/databases /var/lib/neo4j/data/transactions /var/log/neo4j /opt/neo4j/run && \
chown -R appuser:0 /var/lib/neo4j /var/log/neo4j /etc/neo4j /opt/neo4j/run && \
@ -258,31 +203,11 @@ RUN mkdir -p /var/lib/redis /var/log/redis /etc/redis \
&& chmod g+s /var/lib/redis /var/log/redis /etc/redis \
&& chmod -R g=u /var/lib/redis /var/log/redis /etc/redis
# Set permissions for PostgREST binary
RUN chown appuser:0 /bin/postgrest && chmod u+x /bin/postgrest && chmod g=u /bin/postgrest
RUN touch /tmp/postgrest.conf \
&& chown appuser:0 /tmp/postgrest.conf \
&& chmod 640 /tmp/postgrest.conf
# Create PostgREST data, log, and configuration directories
RUN mkdir -p /var/lib/postgrest /var/log/postgrest /etc/postgrest \
&& chown -R appuser:0 /var/lib/postgrest /var/log/postgrest /etc/postgrest \
&& chmod g+s /var/lib/postgrest /var/log/postgrest /etc/postgrest \
&& chmod -R g=u /var/lib/postgrest /var/log/postgrest /etc/postgrest
ENV HOME=/home/appuser
# Installing git for simple git commands
RUN apt-get update && apt-get install -y git && apt-get clean
# Switch back to appuser
USER appuser
WORKDIR /app
# Dependencies for scripts outside nestjs
RUN npm install dotenv@10.0.0 joi@17.4.1
RUN npm cache clean --force
RUN npm install --prefix server --no-save dotenv@10.0.0 joi@17.4.1 && npm cache clean --force
ENTRYPOINT ["./server/ee-entrypoint.sh"]

View file

@ -1,15 +1,227 @@
#!/bin/bash
set -e
# Start Redis
# service redis-server start
# redis-server /etc/redis/redis.conf
echo "🚀 Starting Try ToolJet container initialization..."
# Start Postgres
# Neo4j configuration
# ----------------------------------
# Default Neo4j environment values
# ----------------------------------
export NEO4J_USER=${NEO4J_USER:-"neo4j"}
export NEO4J_PASSWORD=${NEO4J_PASSWORD:-"appaqvyvRLbeukhFE"}
export NEO4J_AUTH=${NEO4J_AUTH:-"neo4j/appaqvyvRLbeukhFE"}
export NEO4J_URI=${NEO4J_URI:-"bolt://localhost:7687"}
export NEO4J_PLUGINS=${NEO4J_PLUGINS:-'["apoc"]'}
export NEO4J_AUTH
# Extract username and password from NEO4J_AUTH if set
if [ -n "$NEO4J_AUTH" ]; then
# Extract username and password from NEO4J_AUTH (format: username/password)
NEO4J_USERNAME=$(echo "$NEO4J_AUTH" | cut -d'/' -f1)
NEO4J_PASSWORD=$(echo "$NEO4J_AUTH" | cut -d'/' -f2)
# Export these for application use
export NEO4J_USERNAME
export NEO4J_PASSWORD
echo "Neo4j authentication configured with username: $NEO4J_USERNAME" >/dev/null 2>&1
else
echo "NEO4J_AUTH not set, using default authentication" >/dev/null 2>&1
fi
# Check if Neo4j is already initialized and set password if necessary
if [ "$NEO4J_AUTH" != "none" ] && [ -n "$NEO4J_PASSWORD" ]; then
echo "Setting Neo4j initial password..." >/dev/null 2>&1
# Ensure Neo4j is not running before setting the initial password
neo4j stop || true
# Set the initial password using the correct command format for Neo4j 5.x
NEO4J_ADMIN_CMD=$(which neo4j-admin)
NEO4J_VERSION=$(neo4j --version | grep -o "[0-9]\+\.[0-9]\+\.[0-9]\+" | head -n 1)
echo "Detected Neo4j version: $NEO4J_VERSION" >/dev/null 2>&1
# Use version-specific command format
MAJOR_VERSION=$(echo $NEO4J_VERSION | cut -d. -f1)
if [ "$MAJOR_VERSION" -ge "5" ]; then
# For Neo4j 5.x and higher
echo "Using Neo4j 5.x+ password command format" >/dev/null 2>&1
$NEO4J_ADMIN_CMD dbms set-initial-password "$NEO4J_PASSWORD" --require-password-change=false >/dev/null 2>&1 || {
echo "Warning: Could not set Neo4j password, it may already be set" >/dev/null 2>&1
}
else
# For Neo4j 4.x and lower
echo "Using Neo4j 4.x password command format" >/dev/null 2>&1
$NEO4J_ADMIN_CMD set-initial-password "$NEO4J_PASSWORD" >/dev/null 2>&1 || {
echo "Warning: Could not set Neo4j password, it may already be set" >/dev/null 2>&1
}
fi
fi
# Update Neo4j configuration
echo "Configuring Neo4j..." >/dev/null 2>&1
cat > /etc/neo4j/neo4j.conf << EOF
# Neo4j configuration
dbms.security.auth_enabled=true
server.bolt.enabled=true
server.bolt.listen_address=0.0.0.0:7687
server.directories.data=/var/lib/neo4j/data
server.directories.logs=/var/log/neo4j
initial.dbms.default_database=neo4j
server.directories.plugins=/var/lib/neo4j/plugins
server.directories.import=/var/lib/neo4j/import
# APOC Settings
dbms.security.procedures.unrestricted=apoc.*
dbms.security.procedures.allowlist=apoc.*,algo.*,gds.*
EOF
if [ -w "$NEO4J_LOG_DIR" ]; then
chmod -R 770 "$NEO4J_LOG_DIR" || echo "Warning: Could not set log directory permissions" >/dev/null 2>&1
fi
# Start Neo4j
echo "Starting Neo4j service..."
neo4j console >/dev/null 2>&1 &
# Add a wait for Neo4j to be ready with more robust checking
echo "Waiting for Neo4j to be ready..." >/dev/null 2>&1
NEO4J_READY=false
for i in {1..60}; do
# First try standard status check
if neo4j status >/dev/null 2>&1; then
echo "Neo4j is ready 🚀"
NEO4J_READY=true
break
fi
# Also try connecting to the bolt port as a fallback
if command -v nc >/dev/null 2>&1; then
if nc -z localhost 7687 >/dev/null 2>&1; then
echo "Neo4j is ready (port 7687 is open)"
NEO4J_READY=true
break
fi
fi
echo "Waiting for Neo4j to start... ($i/60)" >/dev/null 2>&1
sleep 2
done
if [ "$NEO4J_READY" = false ]; then
echo "WARNING: Neo4j may not be fully started yet, but continuing..."
fi
# Configure PostgreSQL authentication
echo "🔧 Configuring PostgreSQL authentication..."
sed -i 's/^local\s\+all\s\+postgres\s\+\(peer\|md5\)/local all postgres trust/' /etc/postgresql/13/main/pg_hba.conf >/dev/null 2>&1
sed -i 's/^local\s\+all\s\+all\s\+\(peer\|md5\)/local all all trust/' /etc/postgresql/13/main/pg_hba.conf >/dev/null 2>&1
# Start PostgreSQL
echo "📈 Starting PostgreSQL..."
service postgresql start
# Export the PORT variable to be used by the application
# Wait until PostgreSQL is ready
echo "⏳ Waiting for PostgreSQL..."
until pg_isready -h localhost -p 5432; do
echo "PostgreSQL not ready yet, retrying..."
sleep 2
done
# Create user and databases for Temporal
echo "🔧 Creating Temporal DBs and user if needed..."
psql -U postgres -tc "SELECT 1 FROM pg_roles WHERE rolname='tooljet'" | grep -q 1 || \
psql -U postgres -c "CREATE USER tooljet WITH PASSWORD 'postgres' SUPERUSER;" >/dev/null 2>&1
psql -U postgres -tc "SELECT 1 FROM pg_database WHERE datname = 'temporal'" | grep -q 1 || \
psql -U postgres -c "CREATE DATABASE temporal OWNER tooljet;" >/dev/null 2>&1
psql -U postgres -tc "SELECT 1 FROM pg_database WHERE datname = 'temporal_visibility'" | grep -q 1 || \
psql -U postgres -c "CREATE DATABASE temporal_visibility OWNER tooljet;" >/dev/null 2>&1
# Generate Temporal config
echo "🔧 Generating Temporal config..."
mkdir -p /etc/temporal/config
if [ -f /etc/temporal/temporal-server.template.yaml ]; then
envsubst < /etc/temporal/temporal-server.template.yaml > /etc/temporal/config/temporal-server.yaml >/dev/null 2>&1
else
echo "❌ Missing template: /etc/temporal/temporal-server.template.yaml"
exit 1
fi
# Download schema files if not present
if [ ! -d "/etc/temporal/schema/postgresql" ]; then
echo "📥 Downloading Temporal schema files..."
mkdir -p /etc/temporal/schema
cd /tmp
curl -sOL https://github.com/temporalio/temporal/archive/refs/tags/v1.28.0.tar.gz
tar -xzf v1.28.0.tar.gz
cp -r temporal-1.28.0/schema/postgresql /etc/temporal/schema/
rm -rf temporal-1.28.0 v1.28.0.tar.gz
cd /
fi
rm -f /etc/temporal/temporal-sql-tool.yaml ~/.temporal/config.yaml
mkdir -p /tmp/temporal
# Set up schemas
echo "🔧 Setting up Temporal schemas..."
for db in temporal temporal_visibility; do
PGPASSWORD=postgres /usr/bin/temporal-sql-tool --plugin postgres12 \
--ep "localhost" --port 5432 --user tooljet --password postgres \
--database $db setup-schema -v 0.0 >/dev/null 2>&1
schema_dir="/etc/temporal/schema/postgresql/v12"
schema_type=$([ "$db" = "temporal" ] && echo "temporal" || echo "visibility")
PGPASSWORD=postgres /usr/bin/temporal-sql-tool --plugin postgres12 \
--ep "localhost" --port 5432 --user tooljet --password postgres \
--database $db update-schema -d "$schema_dir/$schema_type/versioned" >/dev/null 2>&1
done
echo "✅ Schema setup complete"
# Export default port if not set
export PORT=${PORT:-80}
# Start Temporal Server
echo "🚀 Starting Temporal Server..."
/usr/bin/temporal-server start >/dev/null 2>&1 &
TEMPORAL_PID=$!
# Start Supervisor
exec supervisord -c /etc/supervisor/conf.d/supervisord.conf
echo "🚀 Starting Supervisor..."
supervisord -c /etc/supervisor/conf.d/supervisord.conf &
SUPERVISOR_PID=$!
# Wait for Temporal to become ready
echo "⏳ Waiting for Temporal..."
for i in {1..30}; do
if grpcurl -plaintext localhost:7233 grpc.health.v1.Health/Check >/dev/null 2>&1; then
echo "✅ Temporal is ready"
break
fi
sleep 2
done
# Check if namespace already exists
echo "Checking if Temporal namespace exists..."
if grpcurl -plaintext localhost:7233 temporal.api.workflowservice.v1.WorkflowService/ListNamespaces | grep -q '"name": "default"'; then
echo "Namespace 'default' already exists."
else
# Register the namespace if it doesn't exist
echo "Registering Temporal namespace..."
grpcurl -plaintext -d '{
"namespace": "default",
"description": "Default namespace",
"workflowExecutionRetentionPeriod": "259200s"
}' localhost:7233 temporal.api.workflowservice.v1.WorkflowService/RegisterNamespace
fi
# Wait on background processes
wait $TEMPORAL_PID $SUPERVISOR_PID
# Start worker (last step)
echo "🚀 Starting ToolJet worker..."
npm run worker:prod

View file

@ -1,32 +1,208 @@
#!/bin/bash
set -e
# Install grpcurl if not already installed
if ! command -v grpcurl &> /dev/null; then
echo "grpcurl not found, installing..."
apt update && apt install -y curl \
&& curl -sSL https://github.com/fullstorydev/grpcurl/releases/download/v1.8.0/grpcurl_1.8.0_linux_x86_64.tar.gz | tar -xzv -C /usr/local/bin grpcurl
echo "🚀 Starting Try ToolJet container initialization..."
# Neo4j configuration
# ----------------------------------
# Default Neo4j environment values
# ----------------------------------
export NEO4J_USER=${NEO4J_USER:-"neo4j"}
export NEO4J_PASSWORD=${NEO4J_PASSWORD:-"appaqvyvRLbeukhFE"}
export NEO4J_AUTH=${NEO4J_AUTH:-"neo4j/appaqvyvRLbeukhFE"}
export NEO4J_URI=${NEO4J_URI:-"bolt://localhost:7687"}
export NEO4J_PLUGINS=${NEO4J_PLUGINS:-'["apoc"]'}
export NEO4J_AUTH
# Extract username and password from NEO4J_AUTH if set
if [ -n "$NEO4J_AUTH" ]; then
# Extract username and password from NEO4J_AUTH (format: username/password)
NEO4J_USERNAME=$(echo "$NEO4J_AUTH" | cut -d'/' -f1)
NEO4J_PASSWORD=$(echo "$NEO4J_AUTH" | cut -d'/' -f2)
# Export these for application use
export NEO4J_USERNAME
export NEO4J_PASSWORD
echo "Neo4j authentication configured with username: $NEO4J_USERNAME" >/dev/null 2>&1
else
echo "NEO4J_AUTH not set, using default authentication" >/dev/null 2>&1
fi
# Start Redis
service redis-server start
# Check if Neo4j is already initialized and set password if necessary
if [ "$NEO4J_AUTH" != "none" ] && [ -n "$NEO4J_PASSWORD" ]; then
echo "Setting Neo4j initial password..." >/dev/null 2>&1
# Ensure Neo4j is not running before setting the initial password
neo4j stop || true
# Start Postgres
# Set the initial password using the correct command format for Neo4j 5.x
NEO4J_ADMIN_CMD=$(which neo4j-admin)
NEO4J_VERSION=$(neo4j --version | grep -o "[0-9]\+\.[0-9]\+\.[0-9]\+" | head -n 1)
echo "Detected Neo4j version: $NEO4J_VERSION" >/dev/null 2>&1
# Use version-specific command format
MAJOR_VERSION=$(echo $NEO4J_VERSION | cut -d. -f1)
if [ "$MAJOR_VERSION" -ge "5" ]; then
# For Neo4j 5.x and higher
echo "Using Neo4j 5.x+ password command format" >/dev/null 2>&1
$NEO4J_ADMIN_CMD dbms set-initial-password "$NEO4J_PASSWORD" --require-password-change=false >/dev/null 2>&1 || {
echo "Warning: Could not set Neo4j password, it may already be set" >/dev/null 2>&1
}
else
# For Neo4j 4.x and lower
echo "Using Neo4j 4.x password command format" >/dev/null 2>&1
$NEO4J_ADMIN_CMD set-initial-password "$NEO4J_PASSWORD" >/dev/null 2>&1 || {
echo "Warning: Could not set Neo4j password, it may already be set" >/dev/null 2>&1
}
fi
fi
# Update Neo4j configuration
echo "Configuring Neo4j..." >/dev/null 2>&1
cat > /etc/neo4j/neo4j.conf << EOF
# Neo4j configuration
dbms.security.auth_enabled=true
server.bolt.enabled=true
server.bolt.listen_address=0.0.0.0:7687
server.directories.data=/var/lib/neo4j/data
server.directories.logs=/var/log/neo4j
initial.dbms.default_database=neo4j
server.directories.plugins=/var/lib/neo4j/plugins
server.directories.import=/var/lib/neo4j/import
# APOC Settings
dbms.security.procedures.unrestricted=apoc.*
dbms.security.procedures.allowlist=apoc.*,algo.*,gds.*
EOF
if [ -w "$NEO4J_LOG_DIR" ]; then
chmod -R 770 "$NEO4J_LOG_DIR" || echo "Warning: Could not set log directory permissions" >/dev/null 2>&1
fi
# Start Neo4j
echo "Starting Neo4j service..."
neo4j console >/dev/null 2>&1 &
# Add a wait for Neo4j to be ready with more robust checking
echo "Waiting for Neo4j to be ready..." >/dev/null 2>&1
NEO4J_READY=false
for i in {1..60}; do
# First try standard status check
if neo4j status >/dev/null 2>&1; then
echo "Neo4j is ready 🚀"
NEO4J_READY=true
break
fi
# Also try connecting to the bolt port as a fallback
if command -v nc >/dev/null 2>&1; then
if nc -z localhost 7687 >/dev/null 2>&1; then
echo "Neo4j is ready (port 7687 is open)"
NEO4J_READY=true
break
fi
fi
echo "Waiting for Neo4j to start... ($i/60)" >/dev/null 2>&1
sleep 2
done
if [ "$NEO4J_READY" = false ]; then
echo "WARNING: Neo4j may not be fully started yet, but continuing..."
fi
# Configure PostgreSQL authentication
echo "🔧 Configuring PostgreSQL authentication..."
sed -i 's/^local\s\+all\s\+postgres\s\+\(peer\|md5\)/local all postgres trust/' /etc/postgresql/13/main/pg_hba.conf >/dev/null 2>&1
sed -i 's/^local\s\+all\s\+all\s\+\(peer\|md5\)/local all all trust/' /etc/postgresql/13/main/pg_hba.conf >/dev/null 2>&1
# Start PostgreSQL
echo "📈 Starting PostgreSQL..."
service postgresql start
# Start Temporal Server (SQLite configuration)
echo "Starting Temporal Server..."
/usr/bin/temporal-server -r / -c /etc/temporal/ -e temporal-server start &
# Wait until PostgreSQL is ready
echo "⏳ Waiting for PostgreSQL..."
until pg_isready -h localhost -p 5432; do
echo "PostgreSQL not ready yet, retrying..."
sleep 2
done
# Export the PORT variable to be used by the application
# Create user and databases for Temporal
echo "🔧 Creating Temporal DBs and user if needed..."
psql -U postgres -tc "SELECT 1 FROM pg_roles WHERE rolname='tooljet'" | grep -q 1 || \
psql -U postgres -c "CREATE USER tooljet WITH PASSWORD 'postgres' SUPERUSER;" >/dev/null 2>&1
psql -U postgres -tc "SELECT 1 FROM pg_database WHERE datname = 'temporal'" | grep -q 1 || \
psql -U postgres -c "CREATE DATABASE temporal OWNER tooljet;" >/dev/null 2>&1
psql -U postgres -tc "SELECT 1 FROM pg_database WHERE datname = 'temporal_visibility'" | grep -q 1 || \
psql -U postgres -c "CREATE DATABASE temporal_visibility OWNER tooljet;" >/dev/null 2>&1
# Generate Temporal config
echo "🔧 Generating Temporal config..."
mkdir -p /etc/temporal/config
if [ -f /etc/temporal/temporal-server.template.yaml ]; then
envsubst < /etc/temporal/temporal-server.template.yaml > /etc/temporal/config/temporal-server.yaml >/dev/null 2>&1
else
echo "❌ Missing template: /etc/temporal/temporal-server.template.yaml"
exit 1
fi
# Download schema files if not present
if [ ! -d "/etc/temporal/schema/postgresql" ]; then
echo "📥 Downloading Temporal schema files..."
mkdir -p /etc/temporal/schema
cd /tmp
curl -sOL https://github.com/temporalio/temporal/archive/refs/tags/v1.28.0.tar.gz
tar -xzf v1.28.0.tar.gz
cp -r temporal-1.28.0/schema/postgresql /etc/temporal/schema/
rm -rf temporal-1.28.0 v1.28.0.tar.gz
cd /
fi
rm -f /etc/temporal/temporal-sql-tool.yaml ~/.temporal/config.yaml
mkdir -p /tmp/temporal
# Set up schemas
echo "🔧 Setting up Temporal schemas..."
for db in temporal temporal_visibility; do
PGPASSWORD=postgres /usr/bin/temporal-sql-tool --plugin postgres12 \
--ep "localhost" --port 5432 --user tooljet --password postgres \
--database $db setup-schema -v 0.0 >/dev/null 2>&1
schema_dir="/etc/temporal/schema/postgresql/v12"
schema_type=$([ "$db" = "temporal" ] && echo "temporal" || echo "visibility")
PGPASSWORD=postgres /usr/bin/temporal-sql-tool --plugin postgres12 \
--ep "localhost" --port 5432 --user tooljet --password postgres \
--database $db update-schema -d "$schema_dir/$schema_type/versioned" >/dev/null 2>&1
done
echo "✅ Schema setup complete"
# Export default port if not set
export PORT=${PORT:-80}
# Start Supervisor
exec supervisord -c /etc/supervisor/conf.d/supervisord.conf &
# Start Temporal Server
echo "🚀 Starting Temporal Server..."
/usr/bin/temporal-server start >/dev/null 2>&1 &
TEMPORAL_PID=$!
# Wait for Temporal Server to be ready
echo "Waiting for Temporal Server to be ready..."
sleep 10
# Start Supervisor
echo "🚀 Starting Supervisor..."
supervisord -c /etc/supervisor/conf.d/supervisord.conf &
SUPERVISOR_PID=$!
# Wait for Temporal to become ready
echo "⏳ Waiting for Temporal..."
for i in {1..30}; do
if grpcurl -plaintext localhost:7233 grpc.health.v1.Health/Check >/dev/null 2>&1; then
echo "✅ Temporal is ready"
break
fi
sleep 2
done
# Check if namespace already exists
echo "Checking if Temporal namespace exists..."
@ -42,6 +218,9 @@ else
}' localhost:7233 temporal.api.workflowservice.v1.WorkflowService/RegisterNamespace
fi
# Run the worker process (last step)
echo "Starting worker process..."
npm run worker:prod
# Wait on background processes
wait $TEMPORAL_PID $SUPERVISOR_PID
# Start worker (last step)
echo "🚀 Starting ToolJet worker..."
npm --prefix server run worker:prod

View file

@ -1,20 +1,19 @@
FROM tooljet/tooljet:ee-lts-latest
# Copy PostgREST executable
# Copy postgrest executable
COPY --from=postgrest/postgrest:v12.2.0 /bin/postgrest /bin
# Install PostgreSQL
# Install Postgres
USER root
RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ bullseye-pgdg main" | tee /etc/apt/sources.list.d/pgdg.list
RUN echo "deb http://deb.debian.org/debian"
RUN apt update && apt -y install postgresql-13 postgresql-client-13 supervisor
USER postgres
RUN service postgresql start && \
psql -c "create role tooljet with login superuser password 'postgres';"
USER root
# Install Redis
RUN apt update && apt -y install redis
# Create appuser home & ensure permission for supervisord and services
@ -22,6 +21,49 @@ RUN mkdir -p /var/log/supervisor /var/run/postgresql /var/lib/postgresql /var/li
chown -R appuser:appuser /etc/supervisor /var/log/supervisor /var/lib/redis && \
chown -R postgres:postgres /var/run/postgresql /var/lib/postgresql
# Install Temporal Server Binaries
RUN curl -OL https://github.com/temporalio/temporal/releases/download/v1.28.0/temporal_1.28.0_linux_amd64.tar.gz \
&& tar -xzf temporal_1.28.0_linux_amd64.tar.gz \
&& mv temporal-server /usr/bin/temporal-server \
&& mv temporal-sql-tool /usr/bin/temporal-sql-tool \
&& chmod +x /usr/bin/temporal-server /usr/bin/temporal-sql-tool \
&& rm temporal_1.28.0_linux_amd64.tar.gz
# Install Temporal UI Server Binaries
RUN curl -OL https://github.com/temporalio/ui-server/releases/download/v2.28.0/ui-server_2.28.0_linux_amd64.tar.gz && \
tar -xzf ui-server_2.28.0_linux_amd64.tar.gz && \
mv ui-server /usr/bin/temporal-ui-server && \
chmod +x /usr/bin/temporal-ui-server && \
rm ui-server_2.28.0_linux_amd64.tar.gz
# Install Git for schema extraction
RUN apt update && apt install -y git && \
git clone --depth 1 --branch v1.28.0 https://github.com/temporalio/temporal.git /tmp/temporal && \
mkdir -p /etc/temporal/schema/postgresql && \
cp -r /tmp/temporal/schema/postgresql/v12 /etc/temporal/schema/postgresql/ && \
rm -rf /tmp/temporal
# Install envsubst and grpcurl
RUN apt update && apt install -y gettext-base curl \
&& curl -sSL https://github.com/fullstorydev/grpcurl/releases/download/v1.8.0/grpcurl_1.8.0_linux_x86_64.tar.gz | tar -xzv -C /usr/local/bin grpcurl
# Copy Temporal configuration files
COPY ./docker/ee/temporal-server.yaml /etc/temporal/temporal-server.template.yaml
COPY ./docker/ee/temporal-ui-server.yaml /etc/temporal/temporal-ui-server.yaml
# Install Neo4j + APOC
RUN wget -O - https://debian.neo4j.com/neotechnology.gpg.key | apt-key add - && \
echo "deb https://debian.neo4j.com stable 5" > /etc/apt/sources.list.d/neo4j.list && \
apt-get update && apt-get install -y neo4j=1:5.26.6 && apt-mark hold neo4j && \
mkdir -p /var/lib/neo4j/plugins && \
wget -P /var/lib/neo4j/plugins https://github.com/neo4j/apoc/releases/download/5.26.6/apoc-5.26.6-core.jar && \
echo "dbms.security.procedures.unrestricted=apoc.*" >> /etc/neo4j/neo4j.conf && \
echo "dbms.security.procedures.allowlist=apoc.*,algo.*,gds.*" >> /etc/neo4j/neo4j.conf && \
echo "dbms.directories.plugins=/var/lib/neo4j/plugins" >> /etc/neo4j/neo4j.conf && \
echo "dbms.security.auth_enabled=true" >> /etc/neo4j/neo4j.conf && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# Configure Supervisor to manage PostgREST, ToolJet, and Redis
RUN echo "[supervisord] \n" \
"nodaemon=true \n" \
@ -54,6 +96,7 @@ RUN echo "[supervisord] \n" \
# ENV defaults
ENV TOOLJET_HOST=http://localhost \
TOOLJET_SERVER_URL=http://localhost \
PORT=80 \
NODE_ENV=production \
LOCKBOX_MASTER_KEY=replace_with_lockbox_master_key \
@ -62,6 +105,7 @@ ENV TOOLJET_HOST=http://localhost \
PG_USER=tooljet \
PG_PASS=postgres \
PG_HOST=localhost \
PG_PORT=5432 \
ENABLE_TOOLJET_DB=true \
TOOLJET_DB_HOST=localhost \
TOOLJET_DB_USER=tooljet \
@ -78,7 +122,18 @@ ENV TOOLJET_HOST=http://localhost \
REDIS_PORT=6379 \
REDIS_USER=default \
REDIS_PASS= \
TERM=xterm
ENABLE_MARKETPLACE_FEATURE=true \
TERM=xterm \
ENABLE_WORKFLOW_SCHEDULING=true \
TEMPORAL_SERVER_ADDRESS=localhost:7233 \
TEMPORAL_TASK_QUEUE_NAME_FOR_WORKFLOWS=tooljet-workflows \
TOOLJET_WORKFLOWS_TEMPORAL_NAMESPACE=default \
TEMPORAL_ADDRESS=localhost:7233 \
TEMPORAL_DB_HOST=localhost \
TEMPORAL_DB_PORT=5432 \
TEMPORAL_DB_USER=tooljet \
TEMPORAL_DB_PASS=postgres \
TEMPORAL_CORS_ORIGINS=http://localhost:8080
# Set the entrypoint
COPY ./docker/ee/ee-try-entrypoint-lts.sh /ee-try-entrypoint-lts.sh

View file

@ -14,7 +14,6 @@ RUN service postgresql start && \
psql -c "create role tooljet with login superuser password 'postgres';"
USER root
RUN apt update && apt -y install redis
# Create appuser home & ensure permission for supervisord and services
@ -23,11 +22,12 @@ RUN mkdir -p /var/log/supervisor /var/run/postgresql /var/lib/postgresql /var/li
chown -R postgres:postgres /var/run/postgresql /var/lib/postgresql
# Install Temporal Server Binaries
RUN curl -OL https://github.com/temporalio/temporal/releases/download/v1.24.2/temporal_1.24.2_linux_amd64.tar.gz && \
tar -xzf temporal_1.24.2_linux_amd64.tar.gz && \
mv temporal-server /usr/bin/temporal-server && \
chmod +x /usr/bin/temporal-server && \
rm temporal_1.24.2_linux_amd64.tar.gz
RUN curl -OL https://github.com/temporalio/temporal/releases/download/v1.28.0/temporal_1.28.0_linux_amd64.tar.gz \
&& tar -xzf temporal_1.28.0_linux_amd64.tar.gz \
&& mv temporal-server /usr/bin/temporal-server \
&& mv temporal-sql-tool /usr/bin/temporal-sql-tool \
&& chmod +x /usr/bin/temporal-server /usr/bin/temporal-sql-tool \
&& rm temporal_1.28.0_linux_amd64.tar.gz
# Install Temporal UI Server Binaries
RUN curl -OL https://github.com/temporalio/ui-server/releases/download/v2.28.0/ui-server_2.28.0_linux_amd64.tar.gz && \
@ -36,13 +36,33 @@ RUN curl -OL https://github.com/temporalio/ui-server/releases/download/v2.28.0/u
chmod +x /usr/bin/temporal-ui-server && \
rm ui-server_2.28.0_linux_amd64.tar.gz
# Install Git for schema extraction
RUN apt update && apt install -y git && \
git clone --depth 1 --branch v1.28.0 https://github.com/temporalio/temporal.git /tmp/temporal && \
mkdir -p /etc/temporal/schema/postgresql && \
cp -r /tmp/temporal/schema/postgresql/v12 /etc/temporal/schema/postgresql/ && \
rm -rf /tmp/temporal
# Install envsubst and grpcurl
RUN apt update && apt install -y gettext-base curl \
&& curl -sSL https://github.com/fullstorydev/grpcurl/releases/download/v1.8.0/grpcurl_1.8.0_linux_x86_64.tar.gz | tar -xzv -C /usr/local/bin grpcurl
# Copy Temporal configuration files
COPY ./docker/ee/temporal-server.yaml /etc/temporal/temporal-server.yaml
COPY ./docker/ee/temporal-server.yaml /etc/temporal/temporal-server.template.yaml
COPY ./docker/ee/temporal-ui-server.yaml /etc/temporal/temporal-ui-server.yaml
# Install grpcurl
RUN apt update && apt install -y curl \
&& curl -sSL https://github.com/fullstorydev/grpcurl/releases/download/v1.8.0/grpcurl_1.8.0_linux_x86_64.tar.gz | tar -xzv -C /usr/local/bin grpcurl
# Install Neo4j + APOC
RUN wget -O - https://debian.neo4j.com/neotechnology.gpg.key | apt-key add - && \
echo "deb https://debian.neo4j.com stable 5" > /etc/apt/sources.list.d/neo4j.list && \
apt-get update && apt-get install -y neo4j=1:5.26.6 && apt-mark hold neo4j && \
mkdir -p /var/lib/neo4j/plugins && \
wget -P /var/lib/neo4j/plugins https://github.com/neo4j/apoc/releases/download/5.26.6/apoc-5.26.6-core.jar && \
echo "dbms.security.procedures.unrestricted=apoc.*" >> /etc/neo4j/neo4j.conf && \
echo "dbms.security.procedures.allowlist=apoc.*,algo.*,gds.*" >> /etc/neo4j/neo4j.conf && \
echo "dbms.directories.plugins=/var/lib/neo4j/plugins" >> /etc/neo4j/neo4j.conf && \
echo "dbms.security.auth_enabled=true" >> /etc/neo4j/neo4j.conf && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# Configure Supervisor to manage PostgREST, ToolJet, and Redis
RUN echo "[supervisord] \n" \
@ -74,7 +94,6 @@ RUN echo "[supervisord] \n" \
"stdout_logfile=/dev/stdout \n" \
"stdout_logfile_maxbytes=0 \n" | sed 's/ //' > /etc/supervisor/conf.d/supervisord.conf
# ENV defaults
ENV TOOLJET_HOST=http://localhost \
TOOLJET_SERVER_URL=http://localhost \
@ -86,6 +105,7 @@ ENV TOOLJET_HOST=http://localhost \
PG_USER=tooljet \
PG_PASS=postgres \
PG_HOST=localhost \
PG_PORT=5432 \
ENABLE_TOOLJET_DB=true \
TOOLJET_DB_HOST=localhost \
TOOLJET_DB_USER=tooljet \
@ -109,9 +129,13 @@ ENV TOOLJET_HOST=http://localhost \
TEMPORAL_TASK_QUEUE_NAME_FOR_WORKFLOWS=tooljet-workflows \
TOOLJET_WORKFLOWS_TEMPORAL_NAMESPACE=default \
TEMPORAL_ADDRESS=localhost:7233 \
TEMPORAL_DB_HOST=localhost \
TEMPORAL_DB_PORT=5432 \
TEMPORAL_DB_USER=tooljet \
TEMPORAL_DB_PASS=postgres \
TEMPORAL_CORS_ORIGINS=http://localhost:8080
# Set the entrypoint
COPY ./docker/ee/ee-try-entrypoint.sh /ee-try-entrypoint.sh
RUN chmod +x /ee-try-entrypoint.sh
ENTRYPOINT ["/ee-try-entrypoint.sh"]
ENTRYPOINT ["/ee-try-entrypoint.sh"]

View file

@ -3,29 +3,24 @@ log:
level: info
persistence:
defaultStore: sqlite-default
visibilityStore: sqlite-visibility
defaultStore: postgres-default
visibilityStore: postgres-visibility
numHistoryShards: 4
datastores:
sqlite-default:
dataStores:
postgres-default:
sql:
pluginName: "sqlite"
databaseName: "/etc/temporal/default.db"
connectAddr: "localhost"
connectProtocol: "tcp"
connectAttributes:
cache: "private"
setup: true
sqlite-visibility:
pluginName: "postgres12"
databaseName: "temporal"
connectAddr: "localhost:5432"
user: "tooljet"
password: "postgres"
postgres-visibility:
sql:
pluginName: "sqlite"
databaseName: "/etc/temporal/visibility.db"
connectAddr: "localhost"
connectProtocol: "tcp"
connectAttributes:
cache: "private"
setup: true
pluginName: "postgres12"
databaseName: "temporal_visibility"
connectAddr: "localhost:5432"
user: "tooljet"
password: "postgres"
global:
membership:
@ -41,7 +36,7 @@ services:
membershipPort: 6933
bindOnLocalHost: true
httpPort: 7243
matching:
rpc:
grpcPort: 7235
@ -68,8 +63,8 @@ clusterMetadata:
enabled: true
initialFailoverVersion: 1
rpcName: "frontend"
rpcAddress: "localhost:7236"
rpcAddress: "localhost:7233"
httpAddress: "localhost:7243"
dcRedirectionPolicy:
policy: "noop"
policy: "noop"

View file

@ -0,0 +1,9 @@
<svg width="80" height="56" viewBox="0 0 80 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.15" d="M80 0.589844H0V55.4104H80V0.589844Z" fill="#6A727C"/>
<path d="M80 0.589844H0V8.24173H80V0.589844Z" fill="#6A727C"/>
<path d="M4.35492 5.93528C5.19384 5.93528 5.87391 5.25502 5.87391 4.41588C5.87391 3.57674 5.19384 2.89648 4.35492 2.89648C3.51601 2.89648 2.83594 3.57674 2.83594 4.41588C2.83594 5.25502 3.51601 5.93528 4.35492 5.93528Z" fill="white"/>
<path d="M9.3144 5.93528C10.1533 5.93528 10.8334 5.25502 10.8334 4.41588C10.8334 3.57674 10.1533 2.89648 9.3144 2.89648C8.47548 2.89648 7.79541 3.57674 7.79541 4.41588C7.79541 5.25502 8.47548 5.93528 9.3144 5.93528Z" fill="white"/>
<path d="M14.2729 5.93528C15.1118 5.93528 15.7919 5.25502 15.7919 4.41588C15.7919 3.57674 15.1118 2.89648 14.2729 2.89648C13.434 2.89648 12.7539 3.57674 12.7539 4.41588C12.7539 5.25502 13.434 5.93528 14.2729 5.93528Z" fill="white"/>
<path opacity="0.15" d="M17.9315 13.0801H6.89697V49.5456H17.9315V13.0801Z" fill="#6A727C"/>
<path d="M73.1031 13.0801H17.9307V49.5456H73.1031V13.0801Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -43,7 +43,9 @@
"page": "Page",
"searchItem": "Search apps in this workspace",
"workflowsSearchItem": "Search workflows in this workspace",
"searchComponents": "Search components"
"searchComponents": "Search components",
"promote": "Promote",
"release": "Release"
},
"errorBoundary": "Something went wrong.",
"viewer": "Sorry!. This app is under maintenance",

@ -1 +1 @@
Subproject commit 9da4f776915e328120c3024e551ef6b8032f9f63
Subproject commit dbb130bfd859ab795557a36dc26936aa2252e248

File diff suppressed because it is too large Load diff

View file

@ -17,6 +17,7 @@
"@dnd-kit/utilities": "^3.2.1",
"@emoji-mart/data": "^1.1.2",
"@emoji-mart/react": "^1.1.1",
"@mdxeditor/editor": "^3.38.0",
"@microsoft/fetch-event-source": "^2.0.1",
"@radix-ui/colors": "^0.1.8",
"@radix-ui/react-avatar": "^1.0.4",
@ -80,6 +81,7 @@
"papaparse": "^5.3.2",
"path-browserify": "^1.0.1",
"plotly.js-dist-min": "^2.29.1",
"posthog-js": "^1.255.1",
"process": "^0.11.10",
"psl": "^1.9.0",
"query-string": "^8.1.0",
@ -155,6 +157,7 @@
"@babel/plugin-transform-runtime": "^7.19.6",
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.0",
"@storybook/addon-essentials": "^7.2.1",
"@storybook/addon-interactions": "^7.2.1",
"@storybook/addon-links": "^7.2.1",
@ -191,6 +194,7 @@
"postcss": "^8.4.35",
"postcss-loader": "^8.1.0",
"prettier": "^2.8.4",
"react-refresh": "^0.17.0",
"sass": "^1.78.0",
"sass-loader": "^13.2.0",
"storybook": "^7.2.1",
@ -232,7 +236,7 @@
}
},
"scripts": {
"start": "webpack serve --port 8082 --host 0.0.0.0",
"start": "webpack serve --hot --port 8082 --host 0.0.0.0",
"build": "webpack --mode=production && cp -a ./assets/. ./build/assets/",
"lint": "eslint . '**/*.{js,jsx}'",
"format": "eslint . --fix '**/*.{js,jsx}'",

View file

@ -38,6 +38,7 @@ import {
getDataSourcesRoutes,
getAuditLogsRoutes,
} from '@/modules';
import { isWorkflowsFeatureEnabled } from '@/modules/common/helpers/utils';
import { shallow } from 'zustand/shallow';
import useStore from '@/AppBuilder/_stores/store';
import { checkIfToolJetCloud } from '@/_helpers/utils';
@ -112,6 +113,7 @@ class AppComponent extends React.Component {
const featureAccess = await licenseService.getFeatureAccess();
const isBasicPlan = !featureAccess?.licenseStatus?.isLicenseValid || featureAccess?.licenseStatus?.isExpired;
this.setState({ showBanner: isBasicPlan });
this.updateColorScheme();
}
// check if its getting routed from editor
checkPreviousRoute = (route) => {
@ -121,7 +123,7 @@ class AppComponent extends React.Component {
return false;
};
componentDidUpdate(prevProps) {
componentDidUpdate(prevProps, prevState) {
// Check if the current location is the dashboard (homepage)
if (
this.props.location.pathname === `/${getWorkspaceIdOrSlugFromURL()}` &&
@ -134,18 +136,24 @@ class AppComponent extends React.Component {
}
// Update margin when showBanner changes
this.updateMargin();
// Update color scheme if darkMode changed
if (prevState.darkMode !== this.state.darkMode) {
this.updateColorScheme();
}
}
switchDarkMode = (newMode) => {
this.setState({ darkMode: newMode });
this.props.updateIsTJDarkMode(newMode);
localStorage.setItem('darkMode', newMode);
this.updateColorScheme(newMode);
};
isEditorOrViewerFromPath = () => {
const pathname = this.props.location.pathname;
if (pathname.includes('/apps/')) {
return 'editor';
} else if (pathname.includes('/applications/') || pathname.includes('/embed-apps/')) {
}
if (pathname.includes('/applications/') || pathname.includes('/embed-apps/')) {
return 'viewer';
}
return '';
@ -156,6 +164,14 @@ class AppComponent extends React.Component {
isExistingPlanUser = (date) => {
return new Date(date) < new Date('2025-04-24'); //show banner if user created before 2 april (24 for testing)
};
updateColorScheme = (darkModeValue) => {
const isDark = darkModeValue !== undefined ? darkModeValue : this.state.darkMode;
if (isDark) {
document.documentElement.style.setProperty('color-scheme', 'dark');
} else {
document.documentElement.style.removeProperty('color-scheme');
}
};
render() {
const { updateAvailable, darkMode, isEditorOrViewer, showBanner } = this.state;
const mergedProps = {
@ -278,23 +294,30 @@ class AppComponent extends React.Component {
</PrivateRoute>
}
/>
{window.public_config?.ENABLE_WORKFLOWS_FEATURE === 'true' && (
{isWorkflowsFeatureEnabled() && (
<Route
exact
path="/:workspaceId/workflows/*"
element={
<PrivateRoute>
<Workflows switchDarkMode={this.switchDarkMode} darkMode={this.darkMode} />
<Workflows switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
</PrivateRoute>
}
/>
)}
<Route path="/:workspaceId/workspace-settings/*" element={<WorkspaceSettings {...mergedProps} />} />
<Route
path="/:workspaceId/workspace-settings/*"
element={<WorkspaceSettings {...mergedProps} />}
></Route>
<Route path="settings/*" element={<InstanceSettings {...this.props} />}></Route>
<Route path="/:workspaceId/settings/*" element={<Settings {...this.props} />}></Route>
path="settings/*"
element={
<InstanceSettings switchDarkMode={this.switchDarkMode} darkMode={darkMode} {...this.props} />
}
/>
<Route
path="/:workspaceId/settings/*"
element={
<InstanceSettings {...this.props} darkMode={darkMode} switchDarkMode={this.switchDarkMode} />
}
/>
<Route
exact
path="/:workspaceId/modules"
@ -417,7 +440,7 @@ class AppComponent extends React.Component {
/>
</Routes>
</BreadCrumbContext.Provider>
<div id="modal-div"></div>
<div id="modal-div" />
</div>
<Toast toastOptions={toastOptions} />

View file

@ -17,6 +17,8 @@ import { ModuleProvider } from '@/AppBuilder/_contexts/ModuleContext';
import RightSidebarToggle from '@/AppBuilder/RightSideBar/RightSidebarToggle';
import { shallow } from 'zustand/shallow';
import ArtifactPreview from './ArtifactPreview';
// const EditorHeader = lazy(() => import('@/AppBuilder/Header'));
// const LeftSidebar = lazy(() => import('@/AppBuilder/LeftSidebar'));
// const AppCanvas = lazy(() => import('@/AppBuilder/AppCanvas'));
@ -32,6 +34,9 @@ export const Editor = ({ id: appId, darkMode, moduleId = 'canvas', switchDarkMod
const isModuleEditor = appType === 'module';
const updateIsTJDarkMode = useStore((state) => state.updateIsTJDarkMode, shallow);
const appBuilderMode = useStore((state) => state.appStore.modules[moduleId]?.app?.appBuilderMode ?? 'visual');
const isUserInZeroToOneFlow = appBuilderMode === 'ai';
const changeToDarkMode = (newMode) => {
updateIsTJDarkMode(newMode);
@ -51,17 +56,29 @@ export const Editor = ({ id: appId, darkMode, moduleId = 'canvas', switchDarkMod
<ErrorBoundary>
<ModuleProvider moduleId={moduleId} appType={appType} isModuleMode={false} isModuleEditor={isModuleEditor}>
<Suspense fallback={<div>Loading...</div>}>
<EditorHeader darkMode={darkMode} />
<LeftSidebar switchDarkMode={changeToDarkMode} darkMode={darkMode} />
<EditorHeader darkMode={darkMode} isUserInZeroToOneFlow={isUserInZeroToOneFlow} />
<LeftSidebar
switchDarkMode={changeToDarkMode}
darkMode={darkMode}
isUserInZeroToOneFlow={isUserInZeroToOneFlow}
/>
</Suspense>
{window?.public_config?.ENABLE_MULTIPLAYER_EDITING === 'true' && <RealtimeCursors />}
<DndProvider backend={HTML5Backend}>
<AppCanvas moduleId={moduleId} appId={appId} switchDarkMode={switchDarkMode} darkMode={darkMode} />
<QueryPanel darkMode={darkMode} />
<RightSidebarToggle darkMode={darkMode} />
{isRightSidebarOpen && <RightSideBar darkMode={darkMode} />}{' '}
</DndProvider>
<Popups darkMode={darkMode} />
{isUserInZeroToOneFlow ? (
<ArtifactPreview darkMode={darkMode} isUserInZeroToOneFlow={isUserInZeroToOneFlow} />
) : (
<>
{window?.public_config?.ENABLE_MULTIPLAYER_EDITING === 'true' && <RealtimeCursors />}
<DndProvider backend={HTML5Backend}>
<AppCanvas moduleId={moduleId} appId={appId} switchDarkMode={switchDarkMode} darkMode={darkMode} />
<QueryPanel darkMode={darkMode} />
<RightSidebarToggle darkMode={darkMode} />
{isRightSidebarOpen && <RightSideBar darkMode={darkMode} />}{' '}
</DndProvider>
<Popups darkMode={darkMode} />
</>
)}
</ModuleProvider>
</ErrorBoundary>
</div>

View file

@ -8,7 +8,13 @@ import './appCanvas.scss';
import useStore from '@/AppBuilder/_stores/store';
import { shallow } from 'zustand/shallow';
import { computeViewerBackgroundColor, getCanvasWidth } from './appCanvasUtils';
import { NO_OF_GRIDS } from './appCanvasConstants';
import {
LEFT_SIDEBAR_WIDTH,
NO_OF_GRIDS,
PAGES_SIDEBAR_WIDTH_COLLAPSED,
PAGES_SIDEBAR_WIDTH_EXPANDED,
RIGHT_SIDEBAR_WIDTH,
} from './appCanvasConstants';
import cx from 'classnames';
import FreezeVersionInfo from '@/AppBuilder/Header/FreezeVersionInfo';
import { computeCanvasContainerHeight } from '../_helpers/editorHelpers';
@ -109,15 +115,15 @@ export const AppCanvas = ({ appId, isViewer = false, switchDarkMode, darkMode })
return () => window.removeEventListener('resize', handleResize);
}, [currentLayout, canvasMaxWidth, isViewerSidebarPinned, moduleId, isRightSidebarOpen]);
useEffect(() => { }, [isViewerSidebarPinned]);
useEffect(() => {}, [isViewerSidebarPinned]);
const canvasContainerStyles = useMemo(() => {
const canvasBgColor =
currentMode === 'view'
? computeViewerBackgroundColor(isAppDarkMode, canvasBgColor)
: !isAppDarkMode
? '#EBEBEF'
: '#2F3C4C';
? '#EBEBEF'
: '#2F3C4C';
if (isModuleMode) {
return {
@ -134,7 +140,7 @@ export const AppCanvas = ({ appId, isViewer = false, switchDarkMode, darkMode })
width: currentMode === 'edit' ? `calc(100% - 96px)` : '100%',
alignItems: 'unset',
justifyContent: 'unset',
borderRight: currentMode === 'edit' && isRightSidebarOpen && '299' + 'px solid',
borderRight: currentMode === 'edit' && isRightSidebarOpen && `300px solid ${canvasBgColor}`,
padding: currentMode === 'edit' && '8px',
paddingBottom: currentMode === 'edit' && '2px',
};
@ -152,15 +158,34 @@ export const AppCanvas = ({ appId, isViewer = false, switchDarkMode, darkMode })
const shouldAdjust = isSidebarOpen || (isRightSidebarOpen && currentMode === 'edit');
if (!shouldAdjust) return '';
let offset;
if (isViewerSidebarPinned) {
offset = position === 'side' ? '352px' : '126px';
if (isViewerSidebarPinned && !isPagesSidebarHidden) {
if (position === 'side' && isSidebarOpen && isRightSidebarOpen && !isPagesSidebarHidden) {
offset = `${LEFT_SIDEBAR_WIDTH + RIGHT_SIDEBAR_WIDTH - PAGES_SIDEBAR_WIDTH_EXPANDED}px`;
} else if (position === 'side' && isSidebarOpen && !isRightSidebarOpen && !isPagesSidebarHidden) {
offset = `${LEFT_SIDEBAR_WIDTH - PAGES_SIDEBAR_WIDTH_EXPANDED}px`;
} else if (position === 'side' && isRightSidebarOpen && !isSidebarOpen && !isPagesSidebarHidden) {
offset = `${RIGHT_SIDEBAR_WIDTH - PAGES_SIDEBAR_WIDTH_EXPANDED}px`;
}
} else {
offset = position === 'side' ? '171px' : '126px';
if (position === 'side' && isSidebarOpen && isRightSidebarOpen && !isPagesSidebarHidden) {
offset = `${LEFT_SIDEBAR_WIDTH + RIGHT_SIDEBAR_WIDTH - PAGES_SIDEBAR_WIDTH_COLLAPSED}px`;
} else if (position === 'side' && isSidebarOpen && !isRightSidebarOpen && !isPagesSidebarHidden) {
offset = `${LEFT_SIDEBAR_WIDTH - PAGES_SIDEBAR_WIDTH_COLLAPSED}px`;
} else if (position === 'side' && isRightSidebarOpen && !isSidebarOpen && !isPagesSidebarHidden) {
offset = `${RIGHT_SIDEBAR_WIDTH - PAGES_SIDEBAR_WIDTH_COLLAPSED}px`;
}
}
return `calc(100vw - ${offset})`;
if ((position === 'top' || isPagesSidebarHidden) && isSidebarOpen && isRightSidebarOpen) {
offset = `${LEFT_SIDEBAR_WIDTH + RIGHT_SIDEBAR_WIDTH}px`;
} else if ((position === 'top' || isPagesSidebarHidden) && isSidebarOpen && !isRightSidebarOpen) {
offset = `${LEFT_SIDEBAR_WIDTH}px`;
} else if ((position === 'top' || isPagesSidebarHidden) && isRightSidebarOpen && !isSidebarOpen) {
offset = `${RIGHT_SIDEBAR_WIDTH}px`;
}
return `calc(100% + ${offset})`;
}
return (
@ -177,12 +202,12 @@ export const AppCanvas = ({ appId, isViewer = false, switchDarkMode, darkMode })
'canvas-container d-flex page-container',
{ 'dark-theme theme-dark': isAppDarkMode, close: !isViewerSidebarPinned },
{ 'overflow-x-auto': currentMode === 'edit' },
{ 'position-top': position === 'top' },
{ 'position-top': position === 'top' || isPagesSidebarHidden },
{ 'overflow-x-hidden': moduleId !== 'canvas' } // Disbling horizontal scroll for modules in view mode
)}
style={canvasContainerStyles}
>
{showOnDesktop && (
{showOnDesktop && appType !== 'module' && (
<PagesSidebarNavigation
showHeader={showHeader}
isMobileDevice={currentLayout === 'mobile'}
@ -202,6 +227,7 @@ export const AppCanvas = ({ appId, isViewer = false, switchDarkMode, darkMode })
scrollbarWidth: 'none',
overflow: 'auto',
width: currentMode === 'view' ? `calc(100% - ${isViewerSidebarPinned ? '0px' : '0px'})` : '100%',
...(appType === 'module' && isModuleMode && { height: 'inherit' }),
}}
className={`app-${appId} _tooljet-page-${getPageId()}`}
>
@ -213,7 +239,7 @@ export const AppCanvas = ({ appId, isViewer = false, switchDarkMode, darkMode })
{environmentLoadingState !== 'loading' && (
<div>
<Container
id="canvas"
id={moduleId}
gridWidth={gridWidth}
canvasWidth={canvasWidth}
canvasHeight={canvasHeight}

View file

@ -7,6 +7,7 @@ import SolidIcon from '@/_ui/Icon/solidIcons/index';
import { ToolTip } from '@/_components/ToolTip';
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
import { DROPPABLE_PARENTS } from '../appCanvasConstants';
import { Tooltip } from 'react-tooltip';
const CONFIG_HANDLE_HEIGHT = 20;
const BUFFER_HEIGHT = 1;
@ -25,6 +26,7 @@ export const ConfigHandle = ({
subContainerIndex,
}) => {
const { moduleId } = useModuleContext();
const isLicenseValid = useStore((state) => state.isLicenseValid(), shallow);
const shouldFreeze = useStore((state) => state.getShouldFreeze());
const componentName = useStore((state) => state.getComponentDefinition(id, moduleId)?.component?.name || '', shallow);
const isMultipleComponentsSelected = useStore(
@ -111,6 +113,9 @@ export const ConfigHandle = ({
}
}
}}
data-tooltip-id={`invalid-license-modules-${componentName?.toLowerCase()}`}
data-tooltip-html="Your plan is expired. <br/> Renew to use the modules."
data-tooltip-place="right"
>
{licenseValid && isRestricted && (
<ToolTip message={getTooltip()} show={licenseValid && isRestricted && !draggingComponentId}>
@ -201,6 +206,15 @@ export const ConfigHandle = ({
</div>
)}
</span>
{/* Tooltip for invalid license on ModuleViewer */}
{!isLicenseValid && componentType === 'ModuleViewer' && (
<Tooltip
id={`invalid-license-modules-${componentName?.toLowerCase()}`}
className="tooltip"
isOpen={_showHandle && componentType === 'ModuleViewer'}
style={{ textAlign: 'center' }}
/>
)}
</div>
);
};

View file

@ -136,7 +136,7 @@ export const HotkeyProvider = ({ children, mode, currentLayout, canvasMaxWidth }
style={{
width: currentLayout == 'mobile' ? '450px' : '100%',
maxWidth: canvasMaxWidth,
// margin: '0 auto',
margin: '0 auto',
transform: 'translateZ(0)',
}}
>

View file

@ -26,6 +26,7 @@
.empty-box-cont{
display: flex;
justify-content: center;
margin: unset !important;
.dotted-cont{
border: 1px dashed var(--indigo8);

View file

@ -14,9 +14,13 @@ export const DEFAULT_CANVAS_WIDTH = 1292;
export const APP_HEADER_HEIGHT = 47;
export const LEFT_SIDEBAR_WIDTH = 348; // exclusive of border
export const LEFT_SIDEBAR_WIDTH = 350;
export const RIGHT_SIDEBAR_WIDTH = 299;
export const RIGHT_SIDEBAR_WIDTH = 300;
export const PAGES_SIDEBAR_WIDTH_EXPANDED = 226;
export const PAGES_SIDEBAR_WIDTH_COLLAPSED = 44;
export const SUBCONTAINER_WIDGETS = ['Container', 'Tabs', 'Listview', 'Kanban', 'Form'];

View file

@ -66,10 +66,10 @@ export const addNewWidgetToTheEditor = (
componentData.definition.properties.moduleVersionId = { value: moduleInfo.versionId };
componentData.definition.properties.moduleEnvironmentId = { value: moduleInfo.environmentId };
componentData.definition.properties.visibility = { value: true };
customLayouts = moduleInfo.moduleContainer.layouts;
customLayouts = moduleInfo?.moduleContainer?.layouts;
const inputItems = Object.values(
moduleInfo.moduleContainer.component.definition.properties?.input_items?.value ?? {}
moduleInfo.moduleContainer?.component.definition.properties?.input_items?.value ?? {}
);
for (const { name, default_value } of inputItems) {

View file

@ -9,6 +9,7 @@ const useSidebarMargin = (canvasContainerRef) => {
const { moduleId } = useModuleContext();
const [editorMarginLeft, setEditorMarginLeft] = useState(0);
const isSidebarOpen = useStore((state) => state.isSidebarOpen, shallow);
const isRightSidebarOpen = useStore((state) => state.isRightSidebarOpen, shallow);
const mode = useStore((state) => state.modeStore.modules[moduleId].currentMode, shallow);
useEffect(() => {
@ -17,10 +18,10 @@ const useSidebarMargin = (canvasContainerRef) => {
}, [isSidebarOpen, mode]);
useEffect(() => {
if (!isEmpty(canvasContainerRef?.current)) {
if (!isEmpty(canvasContainerRef?.current) && isSidebarOpen && canvasContainerRef.current.scrollLeft === 0) {
canvasContainerRef.current.scrollLeft += editorMarginLeft;
}
}, [editorMarginLeft, canvasContainerRef]);
}, [editorMarginLeft, canvasContainerRef, isSidebarOpen]);
return editorMarginLeft;
};

View file

@ -0,0 +1,9 @@
import React from 'react';
import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent';
function ArtifactPreview() {
return <></>;
}
export default withEditionSpecificComponent(ArtifactPreview, 'AiBuilder');

View file

@ -0,0 +1,9 @@
import React from 'react';
import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent';
function FixWithAi() {
return <></>;
}
export default withEditionSpecificComponent(FixWithAi, 'AiBuilder');

View file

@ -56,6 +56,8 @@ const MultiLineCodeEditor = (props) => {
renderCopilot,
setCodeEditorView,
} = props;
const editorRef = useRef(null);
const replaceIdsWithName = useStore((state) => state.replaceIdsWithName, shallow);
const wrapperRef = useRef(null);
const getSuggestions = useStore((state) => state.getSuggestions, shallow);
@ -330,6 +332,11 @@ const MultiLineCodeEditor = (props) => {
}
}
const onAiSuggestionAccept = (newValue) => {
currentValueRef.current = newValue;
onChange(newValue);
};
return (
<div
className={`code-hinter-wrapper position-relative ${isInsideQueryPane ? 'code-editor-query-panel' : ''}`}
@ -337,7 +344,19 @@ const MultiLineCodeEditor = (props) => {
ref={wrapperRef}
>
<div className={`${className} ${darkMode && 'cm-codehinter-dark-themed'}`}>
<CodeHinterBtns view={editorView} isPanelOpen={isSearchPanelOpen} renderCopilot={renderCopilot} />
<CodeHinterBtns
view={editorView}
isPanelOpen={isSearchPanelOpen}
renderCopilot={() =>
renderCopilot?.({
darkMode,
language: lang,
editorRef,
onAiSuggestionAccept,
})
}
/>
<CodeHinter.PopupIcon
callback={handleTogglePopupExapand}
icon="portal-open"
@ -362,6 +381,7 @@ const MultiLineCodeEditor = (props) => {
<ErrorBoundary>
<div className="codehinter-container w-100 " data-cy={`${cyLabel}-input-field`} style={{ height: '100%' }}>
<CodeMirror
ref={editorRef}
value={initialValueWithReplacedIds}
placeholder={placeholder}
height={'100%'}

View file

@ -3,6 +3,7 @@ import { computeCoercion, getCurrentNodeType, hasDeepChildren, resolveReferences
import CodeHinter from '.';
import { copyToClipboard } from '@/_helpers/appUtils';
import { Alert } from '@/_ui/Alert/Alert';
import { Button } from '@/components/ui/Button/Button';
import _, { isEmpty } from 'lodash';
import { handleCircularStructureToJSON, hasCircularDependency, verifyConstant } from '@/_helpers/utils';
import Popover from 'react-bootstrap/Popover';
@ -15,6 +16,9 @@ import { shallow } from 'zustand/shallow';
import { Overlay } from 'react-bootstrap';
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
import { findDefault } from '../_utils/component-properties-validation';
import FixWithAi from './FixWithAi';
const sanitizeLargeDataset = (data, callback) => {
const SIZE_LIMIT_KB = 5 * 1024; // 5 KB in bytes
@ -143,7 +147,7 @@ export const PreviewBox = ({
useEffect(() => {
if (error) {
setErrorStateActive(true);
setErrorMessage(error.message);
setErrorMessage(error);
} else {
setErrorStateActive(false);
setErrorMessage(null);
@ -159,6 +163,8 @@ export const PreviewBox = ({
validationFn
);
const completeErrMessage = Array.isArray(_error) ? _error.join('.') : _error;
const resolvedValue = typeof rawResolvedValue === 'function' ? undefined : rawResolvedValue;
const newValue = typeof rawNewValue === 'function' ? undefined : rawNewValue;
@ -185,7 +191,7 @@ export const PreviewBox = ({
setError(null);
} else if (!valid && !newValue && !resolvedValue && !isSecretError) {
const err = !error ? `Invalid value for ${validationSchema?.schema?.type}` : `${_error}`;
setError({ message: err, value: resolvedValue, type: 'Invalid' });
setError({ message: err, value: resolvedValue, type: 'Invalid', completeErrorMessage: completeErrMessage });
} else {
const jsErrorType = isSecretError
? 'Error'
@ -211,6 +217,7 @@ export const PreviewBox = ({
? JSON.stringify(errValue, reservedKeywordReplacer)
: resolvedValue,
type: isSecretError ? 'Error' : jsErrorType,
completeErrorMessage: completeErrMessage,
});
setCoersionData(null);
}
@ -309,13 +316,118 @@ const PreviewContainer = ({
isPortalOpen,
previewRef,
showPreview,
onAiSuggestionAccept,
...restProps
}) => {
const { validationSchema, isWorkspaceVariable, errorStateActive, previewPlacement, validationFn } = restProps;
const [errorMessage, setErrorMessage] = useState('');
const typeofError = getCurrentNodeType(errorMessage);
const errorMsg = typeofError === 'Array' ? errorMessage[0] : errorMessage;
const {
validationSchema,
isWorkspaceVariable,
errorStateActive,
previewPlacement,
validationFn,
componentId,
paramName,
fieldMeta,
setIsFocused,
currentValue,
} = restProps;
const aiFeaturesEnabled = useStore((state) => state.ai?.aiFeaturesEnabled ?? false);
const fetchErrorFixUsingAi = useStore((state) => state.fetchErrorFixUsingAi);
const clearChatHistory = useStore((state) => state.clearChatHistory);
const componentDefinition = useStore((state) => state.getComponentDefinition(componentId), shallow); // TODO: check if moduleId needs to be passed here
const componentName = componentDefinition?.component?.name;
const componentKey = `${componentName} - ${fieldMeta?.displayName}`;
const chatList = useStore((state) => state.fixWithAiSlice?.[componentId]?.[componentKey]?.chatHistory ?? []);
const [errorMessage, setErrorMessage] = useState(null);
const [popoverToShow, setPopoverToShow] = useState('preview'); // preview | fixWithAI
const errMsg = errorMessage?.message ?? null;
const typeofError = getCurrentNodeType(errMsg);
const errorMsg = typeofError === 'Array' ? errMsg[0] : errMsg;
const darkMode = localStorage.getItem('darkMode') === 'true';
useEffect(() => {
!showPreview && setPopoverToShow('preview');
}, [showPreview]);
useEffect(() => {
setPopoverToShow('preview');
if (chatList?.length) {
clearChatHistory(componentId, componentKey);
}
}, [currentValue]);
const fetchFixUsingAi = () => {
const defaultValue = validationSchema?.defaultValue
? validationSchema?.defaultValue
: validationSchema
? findDefault(validationSchema?.schema ?? {}, errorMessage?.value)
: undefined;
const errorData = {
key: componentKey,
componentId: componentId,
message: errorMessage?.completeErrorMessage,
error: {
resolvedProperty: { [paramName]: errorMessage?.value },
effectiveProperty: { [paramName]: defaultValue },
componentId,
},
};
fetchErrorFixUsingAi(errorData, {
componentDisplayName:
componentDefinition?.component?.displayName ?? componentDefinition?.component?.component ?? componentName,
errorPropertyDisplayName: fieldMeta?.displayName,
customErrMessage: errorMessage?.message,
});
};
const handleFixErrorWithAI = () => {
setPopoverToShow('fixWithAI');
if (!componentId || chatList?.length) {
return;
}
fetchFixUsingAi();
};
const fixWithAIPopover = (
<Popover
bsPrefix="fix-with-ai-popover"
id="popover-basic"
className={`${darkMode && 'dark-theme'} tw-z-[9999] tw-w-96`}
onMouseEnter={() => setCursorInsidePreview(true)}
onMouseLeave={() => setCursorInsidePreview(false)}
>
<Popover.Body
style={{
border: '1px solid var(--slate6)',
padding: 0,
boxShadow: ' 0px 4px 8px 0px #3032331A, 0px 0px 1px 0px #3032330D',
}}
>
<FixWithAi
componentId={componentId}
componentKey={componentKey}
onApplyFix={onAiSuggestionAccept}
onRetry={fetchFixUsingAi}
onClose={() => setIsFocused(false)}
/>
</Popover.Body>
</Popover>
);
const popover = (
<Popover
bsPrefix="codehinter-preview-popover"
@ -350,6 +462,18 @@ const PreviewContainer = ({
<div className="d-flex align-items-center">
<div className="">{errorMsg !== 'null' ? errorMsg : 'Invalid'}</div>
</div>
{aiFeaturesEnabled && (
<Button
size="medium"
variant="outline"
leadingIcon="tooljetai"
className="mt-2"
onClick={handleFixErrorWithAI}
>
Fix with AI
</Button>
)}
</Alert>
</div>
)}
@ -479,7 +603,7 @@ const PreviewContainer = ({
},
}}
>
{(props) => React.cloneElement(popover, props)}
{(props) => React.cloneElement(popoverToShow === 'fixWithAI' ? fixWithAIPopover : popover, props)}
</Overlay>
)}

View file

@ -37,7 +37,7 @@ import Icon from '@/_ui/Icon/solidIcons/index';
const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...restProps }) => {
const { moduleId } = useModuleContext();
const { initialValue, onChange, enablePreview = true, portalProps } = restProps;
const { initialValue, onChange, enablePreview = true, portalProps, paramName } = restProps;
const { validation = {} } = fieldMeta;
const [showPreview, setShowPreview] = useState(false);
const [isFocused, setIsFocused] = useState(false);
@ -146,17 +146,24 @@ const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...r
enablePreview={enablePreview}
currentValue={currentValue}
isFocused={isFocused}
setIsFocused={setIsFocused}
setCursorInsidePreview={setCursorInsidePreview}
componentName={componentName}
validationSchema={validation}
setErrorStateActive={setErrorStateActive}
ignoreValidation={restProps?.ignoreValidation || isEmpty(validation)}
componentId={restProps?.componentId ?? null}
componentId={componentId ?? null}
fieldMeta={fieldMeta}
paramName={paramName}
isWorkspaceVariable={isWorkspaceVariable}
errorStateActive={errorStateActive}
previewPlacement={restProps?.cyLabel === 'canvas-bg-colour' ? 'top' : 'left-start'}
isPortalOpen={restProps?.portalProps?.isOpen}
validationFn={validationFn}
onAiSuggestionAccept={(newValue) => {
setCurrentValue(newValue);
onChange(newValue);
}}
>
<div className="code-editor-basic-wrapper d-flex">
<div className="codehinter-container w-100">
@ -176,6 +183,7 @@ const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...r
showPreview={showPreview}
wrapperRef={wrapperRef}
showSuggestions={showSuggestions}
cursorInsidePreview={cursorInsidePreview}
{...restProps}
/>
</div>
@ -211,6 +219,7 @@ const EditorInput = ({
wrapperRef,
showSuggestions,
setCodeEditorView = null, // Function to set the CodeMirror view
cursorInsidePreview = false,
}) => {
const codeHinterContext = useContext(CodeHinterContext);
const { suggestionList: paramHints } = createReferencesLookup(codeHinterContext, true);
@ -333,7 +342,8 @@ const EditorInput = ({
}, []);
const handleOnBlur = () => {
setShowPreview(false);
!cursorInsidePreview && setShowPreview(false);
if (!delayOnChange) {
setFirstTimeFocus(false);
return onBlurUpdate(currentValue);

View file

@ -16,11 +16,15 @@ const CreateVersionModal = ({
canCommit,
orgGit,
fetchingOrgGit,
handleCommitOnVersionCreation = () => { },
handleCommitOnVersionCreation = () => {},
}) => {
const { moduleId } = useModuleContext();
const [isCreatingVersion, setIsCreatingVersion] = useState(false);
const [versionName, setVersionName] = useState('');
const isGitSyncEnabled =
orgGit?.org_git?.git_ssh?.is_enabled ||
orgGit?.org_git?.git_https?.is_enabled ||
orgGit?.org_git?.git_lab?.is_enabled;
const {
createNewVersionAction,
@ -102,8 +106,8 @@ const CreateVersionModal = ({
});
},
(error) => {
if (error?.data?.code === "23505") {
toast.error("Version name already exists.");
if (error?.data?.code === '23505') {
toast.error('Version name already exists.');
} else {
toast.error(error?.error);
}
@ -172,7 +176,7 @@ const CreateVersionModal = ({
</div>
</div>
{orgGit?.org_git?.is_enabled && (
{isGitSyncEnabled && (
<div className="commit-changes" style={{ marginTop: '-1rem', marginBottom: '2rem' }}>
<div>
<input

View file

@ -45,9 +45,7 @@ const Menu = (props) => {
{props?.selectProps?.value?.appVersionName &&
decodeEntities(props?.selectProps?.value?.appVersionName)}
</div>
<div
className={cx('col-1', { 'disabled-action-tooltip': props?.selectProps?.appCreationMode === 'GIT' })}
>
<div className={cx('col-1', { 'disabled-action-tooltip': !isVersionCreationEnabled })}>
<EditWhite />
</div>
</div>

View file

@ -15,13 +15,16 @@ import UpdatePresenceMultiPlayer from './UpdatePresenceMultiPlayer';
import { ModuleEditorBanner } from '@/modules/Modules/components';
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
export const EditorHeader = ({ darkMode }) => {
import Steps from './Steps';
export const EditorHeader = ({ darkMode, isUserInZeroToOneFlow }) => {
const { moduleId, isModuleEditor } = useModuleContext();
const { isSaving, saveError, isVersionReleased } = useStore(
const { isSaving, saveError, isVersionReleased, aiGenerationMetadata } = useStore(
(state) => ({
isSaving: state.appStore.modules[moduleId].app.isSaving,
saveError: state.appStore.modules[moduleId].app.saveError,
isVersionReleased: state.isVersionReleased,
aiGenerationMetadata: state.appStore.modules[moduleId].app?.aiGenerationMetadata,
}),
shallow
);
@ -80,44 +83,64 @@ export const EditorHeader = ({ darkMode }) => {
<EditAppName />
</div>
</div>
<HeaderActions darkMode={darkMode} />
<div className="d-flex align-items-center">
<div style={{ width: '100px' }}>
<span
className={cx('autosave-indicator tj-text-xsm', {
'autosave-indicator-saving': isSaving,
'text-danger': saveError,
'd-none': isVersionReleased,
})}
data-cy="autosave-indicator"
>
{getSaveIndicator()}
</span>
</div>
{shouldEnableMultiplayer && (
<div className="mx-2 p-2">
<RealtimeAvatars />
</div>
)}
{shouldEnableMultiplayer && <UpdatePresenceMultiPlayer />}
</div>
</div>
{!isModuleEditor && <div className="navbar-seperator"></div>}
</div>
<div className="d-flex align-items-center p-0">
<div className="d-flex version-manager-container p-0 mx-2 align-items-center ">
{!isModuleEditor && (
{isUserInZeroToOneFlow && (
<Steps
steps={aiGenerationMetadata?.steps?.map((step) => ({ label: step.name, value: step.id })) ?? []}
activeStep={aiGenerationMetadata?.active_step}
/>
)}
{!isUserInZeroToOneFlow && (
<>
<AppEnvironments darkMode={darkMode} />
<AppVersionsManager darkMode={darkMode} />
<GitSyncManager />
<HeaderActions darkMode={darkMode} />
<div className="d-flex align-items-center">
<div style={{ width: '100px' }}>
<span
className={cx('autosave-indicator tj-text-xsm', {
'autosave-indicator-saving': isSaving,
'text-danger': saveError,
'd-none': isVersionReleased,
})}
data-cy="autosave-indicator"
>
{getSaveIndicator()}
</span>
</div>
{shouldEnableMultiplayer && (
<div className="mx-2 p-2">
<RealtimeAvatars />
</div>
)}
{shouldEnableMultiplayer && <UpdatePresenceMultiPlayer />}
</div>
</>
)}
</div>
{!isModuleEditor && !isUserInZeroToOneFlow && <div className="navbar-seperator"></div>}
</div>
{!isUserInZeroToOneFlow && (
<div className="d-flex align-items-center p-0">
<div className="d-flex version-manager-container p-0 mx-2 align-items-center ">
{!isModuleEditor && (
<>
<AppEnvironments darkMode={darkMode} />
<AppVersionsManager darkMode={darkMode} />
<GitSyncManager />
</>
)}
</div>
</div>
)}
</div>
<RightTopHeaderButtons isModuleEditor={isModuleEditor} />
<BuildSuggestions />
{!isUserInZeroToOneFlow && (
<>
<RightTopHeaderButtons isModuleEditor={isModuleEditor} />
<BuildSuggestions />
</>
)}
</div>
</div>
</header>

View file

@ -0,0 +1,64 @@
import React, { Children } from 'react';
import { cn } from '@/lib/utils';
import CheckCircle from '@/_ui/Icon/solidIcons/CheckCircle';
import SolidArrow from '@/_ui/Icon/solidIcons/SolidArrow';
import DottedArrow from '@/_ui/Icon/solidIcons/DottedArrow';
function Step({ stepNo, label, active, completed }) {
return (
<div className="tw-flex tw-items-center tw-gap-1.5 tw-px-2.5 tw-py-1">
{completed ? (
<CheckCircle />
) : (
<span
className={cn(
'tw-bg-text-placeholder tw-text-white tw-text-[0.625rem] tw-rounded-full tw-size-3.5 tw-flex tw-justify-center tw-items-center',
{ '!tw-bg-black': active }
)}
>
{stepNo}
</span>
)}
<p
className={cn('tw-text-base tw-text-text-placeholder tw-font-medium tw-mb-0', {
'tw-text-text-primary': completed || active,
})}
>
{label}
</p>
</div>
);
}
function Connector({ completed }) {
if (completed) return <SolidArrow />;
return <DottedArrow />;
}
// sequential steps
export default function Steps({ steps, activeStep }) {
const activeStepIndex = steps.findIndex((step) => step.value === activeStep);
const currentStepIdx = activeStepIndex === -1 ? 0 : activeStepIndex;
return (
<div className="tw-flex tw-items-center tw-gap-1 tw-py-2">
{Children.toArray(
steps.map((step, index) => {
const isActive = index === currentStepIdx;
const isCompleted = index < currentStepIdx;
return (
<>
<Step stepNo={index + 1} label={step.label} active={isActive} completed={isCompleted} />
{index < steps.length - 1 && <Connector completed={isCompleted} />}
</>
);
})
)}
</div>
);
}

View file

@ -31,7 +31,6 @@ const GlobalSettings = ({ darkMode }) => {
</div>
<div style={{ padding: '12px 16px' }} className={cx({ disabled: shouldFreeze })}>
<MaintenanceMode darkMode={darkMode} />
<HideHeaderToggle darkMode={darkMode} />
</div>
<div className={cx({ 'dark-theme': darkMode })}>
<span className="canvas-styles-header">Canvas Styles</span>

View file

@ -24,6 +24,7 @@ export const BaseLeftSidebar = ({
switchDarkMode,
renderAISideBarTrigger = () => null,
renderAIChat = () => null,
isUserInZeroToOneFlow,
}) => {
const { moduleId, isModuleEditor, appType } = useModuleContext();
const [
@ -72,6 +73,11 @@ export const BaseLeftSidebar = ({
};
useEffect(() => {
if (isUserInZeroToOneFlow) {
setPopoverContentHeight(((window.innerHeight - 48) / window.innerHeight) * 100);
return;
}
if (!isDraggingQueryPane) {
setPopoverContentHeight(
((window.innerHeight - (queryPanelHeight == 0 ? 40 : queryPanelHeight) - 45) / window.innerHeight) * 100
@ -80,7 +86,7 @@ export const BaseLeftSidebar = ({
setPopoverContentHeight(100);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [queryPanelHeight, isDraggingQueryPane]);
}, [isUserInZeroToOneFlow, queryPanelHeight, isDraggingQueryPane]);
const renderPopoverContent = () => {
if (selectedSidebarItem === null || !isSidebarOpen) return null;
@ -111,7 +117,7 @@ export const BaseLeftSidebar = ({
/>
);
case 'tooljetai':
return renderAIChat({ darkMode });
return renderAIChat({ darkMode, isUserInZeroToOneFlow });
// case 'datasource':
// return (
// <LeftSidebarDataSources
@ -211,19 +217,24 @@ export const BaseLeftSidebar = ({
tip: 'Build with AI',
ref: setSideBarBtnRefs('tooljetai'),
})}
{renderCommonItems()}
<SidebarItem
icon="settings"
selectedSidebarItem={selectedSidebarItem}
darkMode={darkMode}
// eslint-disable-next-line no-unused-vars
onClick={(e) => handleSelectedSidebarItem('settings')}
className={`left-sidebar-item left-sidebar-layout`}
badge={true}
tip="Settings"
ref={setSideBarBtnRefs('settings')}
isModuleEditor={isModuleEditor}
/>
{!isUserInZeroToOneFlow && (
<>
{renderCommonItems()}
<SidebarItem
icon="settings"
selectedSidebarItem={selectedSidebarItem}
darkMode={darkMode}
// eslint-disable-next-line no-unused-vars
onClick={(e) => handleSelectedSidebarItem('settings')}
className={`left-sidebar-item left-sidebar-layout`}
badge={true}
tip="Settings"
ref={setSideBarBtnRefs('settings')}
isModuleEditor={isModuleEditor}
/>
</>
)}
</>
);
};

View file

@ -14,6 +14,7 @@ import { useQueryPanelActions } from '@/_stores/queryPanelStore';
import { Tooltip } from 'react-tooltip';
import { canCreateDataSource } from '@/_helpers';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import { isWorkflowsFeatureEnabled } from '@/modules/common/helpers/utils';
import '../queryManager.theme.scss';
import useStore from '@/AppBuilder/_stores/store';
import { staticDataSources } from '../constants';
@ -80,7 +81,7 @@ function DataSourcePicker({ darkMode }) {
navigate(`/${workspaceId}/data-sources`);
};
const workflowsEnabled = window.public_config?.ENABLE_WORKFLOWS_FEATURE == 'true';
const workflowsEnabled = isWorkflowsFeatureEnabled();
return (
<>

View file

@ -15,6 +15,7 @@ import { DataBaseSources, ApiSources, CloudStorageSources } from '@/modules/comm
import { canCreateDataSource } from '@/_helpers';
import './../queryManager.theme.scss';
import { DATA_SOURCE_TYPE } from '@/_helpers/constants';
import { isWorkflowsFeatureEnabled } from '@/modules/common/helpers/utils';
import useStore from '@/AppBuilder/_stores/store';
function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSources, onNewNode, defaultDataSources }) {
@ -34,13 +35,12 @@ function DataSourceSelect({ isDisabled, selectRef, closePopup, workflowDataSourc
const createDataQuery = useStore((state) => state.dataQuery.createDataQuery);
const setPreviewData = useStore((state) => state.queryPanel.setPreviewData);
const handleChangeDataSource = (source) => {
console.log({ source });
createDataQuery(source);
setPreviewData(null);
closePopup();
};
const workflowsEnabled = window.public_config?.ENABLE_WORKFLOWS_FEATURE == 'true';
const workflowsEnabled = isWorkflowsFeatureEnabled();
const staticDataSources = workflowsEnabled
? staticDatasources
: staticDatasources.filter((ds) => ds?.kind !== 'workflows');

View file

@ -9,6 +9,7 @@ import { BaseUrl } from './BaseUrl';
import { queryManagerSelectComponentStyle } from '@/_ui/Select/styles';
import CodeHinter from '@/AppBuilder/CodeEditor';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
import './styles.css';
class Restapi extends React.Component {
constructor(props) {
@ -287,14 +288,15 @@ class Restapi extends React.Component {
const { options } = this.state;
const dataSourceURL = this.props.selectedDataSource?.options?.url?.value;
const queryName = this.props.queryName;
const isWorkflowNode = queryName === 'workflowNode';
const currentValue = { label: options.method?.toUpperCase(), value: options.method };
return (
<div className={`${this.props?.queryName !== 'workflowNode' && 'd-flex'} flex-column`}>
<div className={`${!isWorkflowNode && 'd-flex'} flex-column`}>
{this.props.selectedDataSource?.scope == 'global' && <div className="form-label flex-shrink-0"></div>}{' '}
<div className="flex-grow-1 overflow-hidden">
<div className="rest-api-methods-select-element-container">
<div className="d-flex">
<div className={`rest-api-methods-select-element-container ${isWorkflowNode ? 'workflow-rest-api' : ''}`}>
<div className={`d-flex ${isWorkflowNode ? 'mb-2' : ''}`}>
<p
className="text-placeholder font-weight-medium"
style={{ width: '100px', marginRight: '16px', marginBottom: '0px' }}
@ -303,8 +305,11 @@ class Restapi extends React.Component {
</p>
</div>
<div className="d-flex flex-column w-100">
<div className="d-flex flex-row">
<div className={`me-2`} style={{ width: '90px', height: '32px' }}>
<div className={`${isWorkflowNode ? '' : 'd-flex'} flex-row`}>
<div
className={`me-2 ${isWorkflowNode ? 'mb-2' : ''}`}
style={{ width: isWorkflowNode ? '150px' : '90px', height: '32px' }}
>
<label className="font-weight-medium color-slate12">Method</label>
<Select
options={[
@ -320,9 +325,9 @@ class Restapi extends React.Component {
value={currentValue}
defaultValue={{ label: 'GET', value: 'get' }}
placeholder="Method"
width={100}
width={isWorkflowNode ? 150 : 100}
height={32}
styles={this.customSelectStyles(this.props.darkMode, 91)}
styles={this.customSelectStyles(this.props.darkMode, isWorkflowNode ? 150 : 91)}
useCustomStyles={true}
customClassPrefix="restapi-method-select"
onMenuOpen={() => {
@ -335,7 +340,7 @@ class Restapi extends React.Component {
</div>
<div
className={`field rest-methods-url ${dataSourceURL && 'data-source-exists'}`}
style={{ width: 'calc(100% - 214px)' }}
style={{ width: isWorkflowNode ? '100%' : 'calc(100% - 214px)' }}
>
<div className="font-weight-medium color-slate12">URL</div>
<div className="d-flex h-100 w-100">
@ -371,7 +376,7 @@ class Restapi extends React.Component {
</div>
</div>
</div>
<div className={`query-pane-restapi-tabs`}>
<div className={`query-pane-restapi-tabs`} data-workflow={isWorkflowNode ? 'true' : 'false'}>
<Tabs
theme={this.props.darkMode ? 'monokai' : 'default'}
options={this.state.options}
@ -384,6 +389,7 @@ class Restapi extends React.Component {
bodyToggle={this.state.options.body_toggle}
setBodyToggle={this.onBodyToggleChanged}
onInputChange={this.handleInputChange}
isWorkflow={isWorkflowNode}
/>
</div>
</div>

View file

@ -0,0 +1,45 @@
/* Specific styling for workflow modal */
.workflow-rest-api {
display: flex;
flex-direction: column;
}
/* Ensure method and URL fields have full width in workflow node */
.workflow-rest-api .me-2 {
width: 100% !important;
margin-bottom: 16px; /* Increased spacing to avoid label overlap */
}
/* Ensure URL label doesn't overlap with Method dropdown */
.workflow-rest-api .field .font-weight-medium {
margin-bottom: 4px;
display: block;
padding-top: 4px; /* Add space above URL label */
}
/* Fix the method dropdown width and height for workflow */
.workflow-rest-api .me-2 {
width: 150px !important; /* Wider to accommodate "DELETE" and other long options */
height: auto !important;
min-height: 32px;
}
/* Fix Add more button to fit text properly */
.add-params-btn {
width: 100px !important;
padding: 4px 8px;
}
.add-params-btn p {
display: flex;
align-items: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Button fix for workflow */
.workflow-rest-api ~ .query-pane-restapi-tabs .add-params-btn {
width: auto !important;
min-width: 100px;
}

View file

@ -17,6 +17,8 @@ import { useNavigate } from 'react-router-dom';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
import { BulkUploadPrimaryKey } from './BulkUploadPrimaryKey';
import BulkUpsertPrimaryKey from './BulkUpsertPrimaryKey';
import { fetchEdition } from '@/modules/common/helpers/utils';
import config from 'config';
import './styles.scss';
import CodeHinter from '@/AppBuilder/CodeEditor';
@ -49,6 +51,21 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
const [bulkUpdatePrimaryKey, setBulkUpdatePrimaryKey] = useState(() => options['bulk_update_with_primary_key'] || {});
const [bulkUpsertPrimaryKey, setBulkUpsertPrimaryKey] = useState(() => options['bulk_upsert_with_primary_key'] || {});
// Check if SQL mode should be disabled
const isSqlModeDisabled = () => {
// Check legacy environment variable for backward compatibility
if (window.public_config?.TJDB_SQL_MODE_DISABLE === 'true') {
return true;
}
const edition = fetchEdition(config);
if (edition === 'cloud') {
return true;
}
return false;
};
const joinOptions = options['join_table']?.['joins'] || [
{ conditions: { conditionsList: [{ leftField: { table: selectedTableId } }] } },
];
@ -557,7 +574,7 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
<TooljetDatabaseContext.Provider value={value}>
{/* table name dropdown */}
{window.public_config?.TJDB_SQL_MODE_DISABLE !== 'true' && (
{!isSqlModeDisabled() && (
<div
className={cx({ 'col-4': !isHorizontalLayout, 'd-flex tooljetdb-worflow-operations': isHorizontalLayout })}
>

View file

@ -7,6 +7,7 @@ import { v4 as uuidv4 } from 'uuid';
import useStore from '@/AppBuilder/_stores/store';
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
import usePopoverObserver from '@/AppBuilder/_hooks/usePopoverObserver';
import useWorkflowStore from '@/_stores/workflowStore';
export function Workflows({ options, optionsChanged, currentState }) {
const { moduleId } = useModuleContext();
@ -15,7 +16,9 @@ export function Workflows({ options, optionsChanged, currentState }) {
const [_selectedWorkflowId, setSelectedWorkflowId] = useState(undefined);
const [params, setParams] = useState([...(options.params ?? [{ key: '', value: '' }])]);
const appId = useStore((state) => state.appStore.modules[moduleId].app.appId);
const workflowIdFromStore = useWorkflowStore((state) => state.workflowId);
const appIdFromStore = useStore((state) => state.appStore.modules[moduleId].app.appId);
const appId = workflowIdFromStore || appIdFromStore;
usePopoverObserver(
document.getElementsByClassName('query-details')[0],

View file

@ -36,8 +36,8 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
const hasPermissions =
selectedDataSourceScope === 'global'
? canUpdateDataSource(dataQuery?.data_source_id) ||
canReadDataSource(dataQuery?.data_source_id) ||
canDeleteDataSource()
canReadDataSource(dataQuery?.data_source_id) ||
canDeleteDataSource()
: true;
const toggleQueryHandlerMenu = useStore((state) => state.queryPanel.toggleQueryHandlerMenu);
@ -121,9 +121,8 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
{isRenaming ? (
<input
data-cy={`query-edit-input-field`}
className={`query-name query-name-input-field border-indigo-09 bg-transparent ${
darkMode && 'text-white'
}`}
className={`query-name query-name-input-field border-indigo-09 bg-transparent ${darkMode && 'text-white'
}`}
type="text"
defaultValue={decodeEntities(dataQuery.name)}
autoFocus={true}
@ -181,6 +180,7 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
variant="outline"
className=""
id={`query-handler-menu-${dataQuery?.id}`}
data-cy={`delete-query-${dataQuery.name.toLowerCase()}`}
/>
</div>
</div>

View file

@ -155,6 +155,7 @@ export const QueryPanel = ({ darkMode }) => {
justifyContent: 'space-between',
alignItems: 'center',
zIndex: 2,
width: '100%',
}}
>
<div

View file

@ -9,17 +9,21 @@ import SolidIcon from '@/_ui/Icon/SolidIcons';
export const ComponentConfigurationTab = ({ darkMode, isModuleEditor }) => {
const selectedComponentId = useStore((state) => state.selectedComponents?.[0], shallow);
const activeTab = useStore((state) => state.activeRightSideBarTab, shallow);
const toggleRightSidebarPin = useStore((state) => state.toggleRightSidebarPin);
const isRightSidebarPinned = useStore((state) => state.isRightSidebarPinned);
const setRightSidebarOpen = useStore((state) => state.setRightSidebarOpen);
const setActiveRightSideBarTab = useStore((state) => state.setActiveRightSideBarTab);
const handleToggle = () => {
setActiveRightSideBarTab(null);
setRightSidebarOpen(false);
};
if (!selectedComponentId && activeTab !== RIGHT_SIDE_BAR_TAB.PAGES) {
// return setActiveRightSideBarTab(RIGHT_SIDE_BAR_TAB.COMPONENTS);
return (
<>
<div className="empty-configuration-header">
<div className="header">Component properties</div>
<div className="icon-btn cursor-pointer" onClick={() => toggleRightSidebarPin()}>
<SolidIcon fill="var(--icon-strong)" name={isRightSidebarPinned ? 'unpin' : 'pin'} width="16" />
<div className="icon-btn cursor-pointer flex-shrink-0 p-2 h-4 w-4" onClick={handleToggle}>
<SolidIcon fill="var(--icon-strong)" name={'remove03'} width="16" viewBox="0 0 16 16" />
</div>
</div>
<div className="d-flex align-items-center justify-content-center no-component-selected">
@ -39,6 +43,7 @@ export const ComponentConfigurationTab = ({ darkMode, isModuleEditor }) => {
selectedComponentId={selectedComponentId}
pages={[]}
isModuleEditor={isModuleEditor}
handleRightSidebarToggle={handleToggle}
/>
);
};

View file

@ -1,7 +1,7 @@
import React, { useState } from 'react';
import './styles.scss';
export const ComponentModuleTab = ({ onChangeTab }) => {
export const ComponentModuleTab = ({ onChangeTab, hasModuleAccess }) => {
const [activeTab, setActiveTab] = useState(1);
const handleChangeTab = (tab) => {
@ -18,13 +18,15 @@ export const ComponentModuleTab = ({ onChangeTab }) => {
>
<span>Components</span>
</button>
<button
className={`tj-drawer-tabs-btn tj-text-xsm ${activeTab == 2 && 'tj-drawer-tabs-btn-active'}`}
onClick={() => handleChangeTab(2)}
data-cy="button-upload-csv-file"
>
<span>Modules</span>
</button>
{hasModuleAccess && (
<button
className={`tj-drawer-tabs-btn tj-text-xsm ${activeTab == 2 && 'tj-drawer-tabs-btn-active'}`}
onClick={() => handleChangeTab(2)}
data-cy="button-upload-csv-file"
>
<span>Modules</span>
</button>
)}
</div>
</div>
);

View file

@ -1,4 +1,4 @@
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useMemo, useState, useEffect } from 'react';
import { isEmpty, debounce } from 'lodash';
import { useTranslation } from 'react-i18next';
import { LEGACY_ITEMS, IGNORED_ITEMS } from './constants';
@ -12,6 +12,32 @@ import sectionConfig from './sectionConfig';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import { ModuleManager } from '@/modules/Modules/components';
import { ComponentModuleTab } from '@/modules/Appbuilder/components';
import { useLicenseStore } from '@/_stores/licenseStore';
import { shallow } from 'zustand/shallow';
// Simple error boundary component for module errors
class ModuleErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Module error:', error, errorInfo);
this.props.onError();
}
render() {
if (this.state.hasError) {
return null; // Let parent handle the fallback
}
return this.props.children;
}
}
// TODO: Hardcode all the component-section mapping in a constant file and just loop over it
// TODO: styling
@ -28,11 +54,31 @@ export const ComponentsManagerTab = ({ darkMode, isModuleEditor }) => {
const [filteredComponents, setFilteredComponents] = useState(componentList);
const [searchQuery, setSearchQuery] = useState('');
const [activeTab, setActiveTab] = useState(1);
const [moduleError, setModuleError] = useState(false);
const _shouldFreeze = useStore((state) => state.getShouldFreeze());
const isAutoMobileLayout = useStore((state) => state.currentLayout === 'mobile' && state.getIsAutoMobileLayout());
const shouldFreeze = _shouldFreeze || isAutoMobileLayout;
const toggleRightSidebarPin = useStore((state) => state.toggleRightSidebarPin);
const isRightSidebarPinned = useStore((state) => state.isRightSidebarPinned);
const { hasModuleAccess } = useLicenseStore(
(state) => ({
hasModuleAccess: state.hasModuleAccess,
}),
shallow
);
// Force re-render when hasModuleAccess changes
useEffect(() => {
// If modules access is denied and we're on the modules tab, switch to components
if (!hasModuleAccess && activeTab === 2) {
setActiveTab(1);
}
}, [hasModuleAccess, activeTab]);
const setRightSidebarOpen = useStore((state) => state.setRightSidebarOpen);
const activeRightSideBarTab = useStore((state) => state.activeRightSideBarTab);
const setActiveRightSideBarTab = useStore((state) => state.setActiveRightSideBarTab);
const isRightSidebarOpen = useStore((state) => state.isRightSidebarOpen);
const handleSearchQueryChange = useCallback(
debounce((value) => {
setSearchQuery(value);
@ -44,6 +90,11 @@ export const ComponentsManagerTab = ({ darkMode, isModuleEditor }) => {
[activeTab]
);
const handleToggle = () => {
setActiveRightSideBarTab(null);
setRightSidebarOpen(false);
};
const filterComponents = useCallback((value) => {
if (value !== '') {
const fuse = new Fuse(componentList, {
@ -143,25 +194,52 @@ export const ComponentsManagerTab = ({ darkMode, isModuleEditor }) => {
}
const handleChangeTab = (tab) => {
if (tab === 2 && !hasModuleAccess) {
setActiveTab(1);
return;
}
setActiveTab(tab);
if (tab === 1) setModuleError(false);
// When changing tabs, we don't need to reset the search
// The search query will be applied to the new tab
};
// Handle module errors by redirecting to components tab
useEffect(() => {
if (moduleError && activeTab === 2) {
setActiveTab(1);
}
}, [moduleError, activeTab]);
const renderSection = () => {
if (activeTab === 1) {
return <div className="widgets-list col-sm-12 col-lg-12 row">{segregateSections()}</div>;
}
return <ModuleManager searchQuery={searchQuery} />;
// If there was an error accessing modules, redirect to components tab
if (moduleError) {
return <div className="widgets-list col-sm-12 col-lg-12 row">{segregateSections()}</div>;
}
return (
<ModuleErrorBoundary onError={() => setModuleError(true)}>
<ModuleManager searchQuery={searchQuery} />
</ModuleErrorBoundary>
);
};
return (
<div className={`components-container ${shouldFreeze ? 'disabled' : ''}`}>
{isModuleEditor ? (
<p className="widgets-manager-header">Components</p>
) : (
<ComponentModuleTab onChangeTab={handleChangeTab} />
)}
<div className="d-flex align-items-center">
{isModuleEditor ? (
<p className="widgets-manager-header">Components</p>
) : (
<ComponentModuleTab onChangeTab={handleChangeTab} hasModuleAccess={hasModuleAccess} />
)}
<div className="icon-btn cursor-pointer flex-shrink-0 me-3 p-2 h-4 w-4" onClick={handleToggle}>
<SolidIcon fill="var(--icon-strong)" name={'remove03'} width="16" viewBox="0 0 16 16" />
</div>
</div>
<div className="input-icon tj-app-input">
<SearchBox
dataCy={`widget-search-box`}

View file

@ -10,7 +10,7 @@ import { shallow } from 'zustand/shallow';
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
import { noop } from 'lodash';
export const DragLayer = ({ index, component, isModuleTab = false }) => {
export const DragLayer = ({ index, component, isModuleTab = false, disabled = false }) => {
const [isRightSidebarOpen, toggleRightSidebar] = useStore(
(state) => [state.isRightSidebarOpen, state.toggleRightSidebar],
shallow
@ -51,8 +51,16 @@ export const DragLayer = ({ index, component, isModuleTab = false }) => {
return (
<>
{isDragging && <CustomDragLayer size={size} />}
<div ref={drag} className="draggable-box" style={{ height: '100%', width: isModuleTab && '100%' }}>
{isModuleTab ? <ModuleWidgetBox module={component} /> : <WidgetBox index={index} component={component} />}
<div
ref={disabled ? undefined : drag}
className={`draggable-box${disabled ? ' disabled' : ''}`}
style={{ height: '100%', width: isModuleTab && '100%' }}
>
{isModuleTab ? (
<ModuleWidgetBox module={component} disabled={disabled} />
) : (
<WidgetBox index={index} component={component} />
)}
</div>
</>
);

View file

@ -1,4 +1,4 @@
import React, { useState, useCallback, useMemo, useEffect } from 'react';
import React, { useState, useCallback, useMemo, useEffect, useRef } from 'react';
import { Button } from '@/components/ui/Button/Button';
import Checkbox from '@/components/ui/Checkbox/Index';
import cx from 'classnames';
@ -78,10 +78,8 @@ const ModalFooter = ({ currentStatus, refreshData, handleSubmit, isSaving, allSe
* Loader component
*/
const LoaderComponent = () => (
<div style={{ width: '100%', height: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<center>
<Loader width="32" absolute={false} />
</center>
<div className="tw-absolute tw-inset-0 tw-flex tw-items-center tw-justify-center tw-z-10">
<Loader width="32" absolute={false} />
</div>
);
@ -388,19 +386,40 @@ const ColumnMappingComponent = ({
const [isSaving, setIsSaving] = useState(false);
const [refreshedColumns, setRefreshedColumns] = useState([]);
const [showLoader, setShowLoader] = useState(false);
const bodyContainerRef = useRef(null);
const lastBodyHeightRef = useRef(60);
useEffect(() => {
setShowLoader(isDataLoading);
}, [isDataLoading]);
// Track body height when content is loaded
useEffect(() => {
if (!showLoader && bodyContainerRef.current) {
// Use setTimeout to ensure DOM is fully rendered
setTimeout(() => {
if (bodyContainerRef.current) {
const height = bodyContainerRef.current.scrollHeight;
if (height > 0) {
lastBodyHeightRef.current = height;
}
}
}, 0);
}
}, [showLoader, groupedColumns]);
const currentStatus = currentStatusRef.current;
console.log('here--- existingResolvedJsonData--- ', existingResolvedJsonData);
const columnsToUse = useColumnBuilder(
component,
currentStatus,
newResolvedJsonData,
existingResolvedJsonData,
refreshedColumns?.length === 0 ? newResolvedJsonData : refreshedColumns,
refreshedColumns?.length === 0 || Object.keys(refreshedColumns).length === 0
? newResolvedJsonData
: refreshedColumns,
getFormFields,
getComponentDefinition
);
@ -459,7 +478,11 @@ const ColumnMappingComponent = ({
const modalBody = (
<>
<div className="tw-w-full column-mapping-modal-body-container tw-max-h-[500px] tw-overflow-y-auto tw-p-4 tw-pb-0">
<div
ref={bodyContainerRef}
className="tw-w-full column-mapping-modal-body-container tw-max-h-[500px] tw-overflow-y-auto tw-p-4 tw-pb-0 tw-relative"
style={showLoader && lastBodyHeightRef.current ? { minHeight: `${lastBodyHeightRef.current}px` } : undefined}
>
{showLoader && <LoaderComponent />}
{!showLoader && (

View file

@ -26,6 +26,12 @@ export const useFormLogic = (component, paramUpdated) => {
// Save data section function
const saveDataSection = (fields) => {
formState.savedSourceValue.current = formState.source.value;
const newJsonData = formState.JSONData;
if (newJsonData?.value === undefined) {
newJsonData.value = resolveReferences('canvas', formState.source.value);
}
saveFormDataSectionData(
component?.id,
{

View file

@ -29,6 +29,7 @@ export const createParamUpdatedInterceptor = ({
const { generateFormFrom, JSONData } = getFormDataSectionData(component?.id);
if (value === generateFormFrom?.value) {
setSource((prev) => ({ ...prev, value }));
return setJSONData({ value: JSONData.value });
}

View file

@ -256,18 +256,18 @@ export const mergeFormFieldsWithNewData = (existingFields, newFields) => {
const existingFieldsMap = {};
existingFields.forEach((field) => {
if (field.name) {
existingFieldsMap[field.name] = field;
if (field.key) {
existingFieldsMap[field.key] = field;
}
});
return newFields.map((newField) => {
if (newField.isNew || !existingFieldsMap[newField.name]) {
if (newField.isNew || !existingFieldsMap[newField.key]) {
return newField;
}
return {
...newField,
...omit(existingFieldsMap[newField.name], ['isNew']),
...omit(existingFieldsMap[newField.key], ['isNew']),
};
});
};

Some files were not shown because too many files have changed in this diff Show more