mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-05 22:38:48 +00:00
Merge branch 'fix/appbuilder-03' into gh-12680-toggle-app-mode
This commit is contained in:
commit
d02b3d8c5c
620 changed files with 17433 additions and 7134 deletions
|
|
@ -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
133
.github/workflows/cloud-frontend-gcp.yml
vendored
Normal 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
133
.github/workflows/cloud-frontend.yml
vendored
Normal 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
|
||||
109
.github/workflows/docker-release.yml
vendored
109
.github/workflows/docker-release.yml
vendored
|
|
@ -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:
|
||||
|
|
|
|||
57
.github/workflows/manual-docker-build.yml
vendored
Normal file
57
.github/workflows/manual-docker-build.yml
vendored
Normal 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 }}
|
||||
28
.github/workflows/merging-pr.yml
vendored
28
.github/workflows/merging-pr.yml
vendored
|
|
@ -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'
|
||||
|
|
|
|||
10
.github/workflows/packer-build.yml
vendored
10
.github/workflows/packer-build.yml
vendored
|
|
@ -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
2
.nvmrc
|
|
@ -1 +1 @@
|
|||
v18.18.2
|
||||
v22.15.1
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 \
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
(
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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"]`
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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`);
|
||||
|
|
|
|||
|
|
@ -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`);
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 won’t 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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
120
cypress-tests/cypress/support/utils/platform/multiEnv.js
Normal file
120
cypress-tests/cypress/support/utils/platform/multiEnv.js
Normal 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();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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=
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
0
docker/cloud/cloud-entrypoint.sh
Normal file → Executable 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"]
|
||||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
@ -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"
|
||||
9
frontend/assets/images/icons/empty-modules.svg
Normal file
9
frontend/assets/images/icons/empty-modules.svg
Normal 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 |
|
|
@ -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
|
||||
2508
frontend/package-lock.json
generated
2508
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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}'",
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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)',
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
.empty-box-cont{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: unset !important;
|
||||
|
||||
.dotted-cont{
|
||||
border: 1px dashed var(--indigo8);
|
||||
|
|
|
|||
|
|
@ -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'];
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
9
frontend/src/AppBuilder/ArtifactPreview/index.jsx
Normal file
9
frontend/src/AppBuilder/ArtifactPreview/index.jsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import React from 'react';
|
||||
|
||||
import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent';
|
||||
|
||||
function ArtifactPreview() {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
export default withEditionSpecificComponent(ArtifactPreview, 'AiBuilder');
|
||||
9
frontend/src/AppBuilder/CodeEditor/FixWithAi.jsx
Normal file
9
frontend/src/AppBuilder/CodeEditor/FixWithAi.jsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import React from 'react';
|
||||
|
||||
import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent';
|
||||
|
||||
function FixWithAi() {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
export default withEditionSpecificComponent(FixWithAi, 'AiBuilder');
|
||||
|
|
@ -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%'}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
64
frontend/src/AppBuilder/Header/Steps.jsx
Normal file
64
frontend/src/AppBuilder/Header/Steps.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 })}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@ export const QueryPanel = ({ darkMode }) => {
|
|||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
zIndex: 2,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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`}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue