diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index 6bb2f8241f..b5f3acd0d5 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -232,3 +232,95 @@ jobs: # fi # curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }} + + + try-tooljet-image-build: + runs-on: ubuntu-latest + needs: build-tooljet-image-for-ee-edtion + if: ${{ needs.build-tooljet-image-for-ee-edtion.result == 'success' }} + + steps: + - name: Checkout code to develop + if: "!contains(github.event.release.tag_name, 'ee-lts')" + uses: actions/checkout@v2 + with: + ref: refs/heads/main + + - name: Checkout code to lts-3.0 + if: contains(github.event.release.tag_name, '-ee-lts') + uses: actions/checkout@v2 + with: + ref: refs/heads/lts-3.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 + + - name: Set DOCKER_CLI_EXPERIMENTAL + run: echo "DOCKER_CLI_EXPERIMENTAL=enabled" >> $GITHUB_ENV + + - name: use mybuilder buildx + run: docker buildx use mybuilder + + - name: Docker Login + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Check if Docker image is present + id: check-image-presence + run: | + response=$(curl -s "https://hub.docker.com/v2/repositories/tooljet/tooljet/tags/${{ github.event.release.tag_name }}") + if [[ $? -ne 0 ]]; then + echo "Failed to fetch JSON response. Stopping workflow execution." + exit 1 + fi + + if [[ $response == *"tag '${{ github.event.release.tag_name }}' not found"* ]]; then + echo "Docker image tag '${{ github.event.release.tag_name }}' not present." + exit 1 + else + echo "Docker image tag '${{ github.event.release.tag_name }}' is present." + fi + + - name: Build and Push Docker image for non-EE-LTS + if: "!contains(github.event.release.tag_name, '-ee-lts')" + uses: docker/build-push-action@v4 + with: + context: . + file: docker/ee/ee-try-tooljet.Dockerfile + push: true + tags: tooljet/try:${{ github.event.release.tag_name }},tooljet/try:ee-latest + platforms: linux/amd64 + env: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and Push Docker image for EE-LTS-3.0 + if: contains(github.event.release.tag_name, '-ee-lts') + uses: docker/build-push-action@v4 + with: + context: . + file: docker/ee/ee-try-tooljet-lts.Dockerfile + push: true + tags: tooljet/try:${{ github.event.release.tag_name }},tooljet/try:ee-lts-latest + 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="Try-ToolJet image published:\\n\`tooljet/try:${{ github.event.release.tag_name }}\`" + else + message="Job '${{ env.JOB_NAME }}' failed! tooljet/try:${{ github.event.release.tag_name }}" + fi + + curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$message\"}" ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/.github/workflows/render-suspend-labeler.yml b/.github/workflows/render-suspend-labeler.yml index 7860ae3ade..e9dd2e8b9b 100644 --- a/.github/workflows/render-suspend-labeler.yml +++ b/.github/workflows/render-suspend-labeler.yml @@ -8,16 +8,16 @@ permissions: issues: write jobs: - label-stale-deploys: + label-stale-ce-deploys: runs-on: ubuntu-latest permissions: - pull-requests: write + pull-requests: write steps: - uses: akshaysasidrn/stale-label-fetch@v1.1 id: stale-label with: github-token: ${{ secrets.GITHUB_TOKEN }} - stale-label: 'active-review-app' + stale-label: 'active-ce-review-app' stale-time: '86400' type: 'pull_request' - name: Get stale numbers @@ -40,6 +40,42 @@ jobs: issue_number: prNumber, owner: context.repo.owner, repo: context.repo.repo, - labels: ['suspend-review-app'] + labels: ['suspend-ce-review-app'] + }) + } + + label-stale-ee-deploys: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - uses: akshaysasidrn/stale-label-fetch@v1.1 + id: stale-label + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + stale-label: 'active-ee-review-app' + stale-time: '86400' + type: 'pull_request' + - name: Get stale numbers + run: echo "Matched PR numbers - ${{ steps.stale-label.outputs.stale-numbers }}" + - name: Add suspend label + uses: actions/github-script@v6 + env: + STALE_NUMBERS: ${{ steps.stale-label.outputs.stale-numbers }} + with: + github-token: ${{ secrets.TJ_BOT_PAT }} + script: | + if (!process.env.STALE_NUMBERS) return + + const prNumbers = process.env.STALE_NUMBERS.split(",") + + console.log(`Adding suspend labels for: ${prNumbers}`) + + for (const prNumber of prNumbers) { + github.rest.issues.addLabels({ + issue_number: prNumber, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['suspend-ee-review-app'] }) } diff --git a/.github/workflows/vulnerability-ci.yml b/.github/workflows/vulnerability-ci.yml index 15f8425a46..568ab6df31 100644 --- a/.github/workflows/vulnerability-ci.yml +++ b/.github/workflows/vulnerability-ci.yml @@ -11,7 +11,7 @@ on: # Schedule the workflow to run every two weeks once schedule: - - cron: '30 5 */14 * *' + - cron: '30 5 * * 1' jobs: PeriodicVulnerability-CheckOn-frontend-code: diff --git a/docker/ce-entrypoint.sh b/docker/ce-entrypoint.sh new file mode 100755 index 0000000000..4b63af2e45 --- /dev/null +++ b/docker/ce-entrypoint.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -e + +if [ -d "./server/dist" ]; then + SETUP_CMD='npm run db:setup:prod' +else + SETUP_CMD='npm run db:setup' +fi + +if [ -f "./.env" ]; then + declare $(grep -v '^#' ./.env | xargs) +fi + +if [ -z "$DATABASE_URL" ]; then + ./server/scripts/wait-for-it.sh $PG_HOST:${PG_PORT:-5432} --strict --timeout=300 -- $SETUP_CMD +else + PG_HOST=$(echo "$DATABASE_URL" | awk -F'[/:@?]' '{print $6}') + PG_PORT=$(echo "$DATABASE_URL" | awk -F'[/:@?]' '{print $7}') + + if [ -z "$DATABASE_PORT" ]; then + DATABASE_PORT="5432" + fi + + ./server/scripts/wait-for-it.sh "$PG_HOST:$PG_PORT" --strict --timeout=300 -- $SETUP_CMD +fi + +exec "$@" diff --git a/docker/ce-production.Dockerfile b/docker/ce-production.Dockerfile index 4e70ecb882..c77ebf128e 100644 --- a/docker/ce-production.Dockerfile +++ b/docker/ce-production.Dockerfile @@ -88,12 +88,13 @@ 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/entrypoint.sh ./app/server/entrypoint.sh 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 ./docker/ce-entrypoint.sh ./app/server/entrypoint.sh + # Define non-sudo user RUN useradd --create-home --home-dir /home/appuser appuser \ && chown -R appuser:0 /app \ @@ -111,5 +112,4 @@ WORKDIR /app # Dependencies for scripts outside nestjs RUN npm install dotenv@10.0.0 joi@17.4.1 - ENTRYPOINT ["./server/entrypoint.sh"] diff --git a/server/entrypoint.sh b/docker/ee/ee-entrypoint.sh similarity index 100% rename from server/entrypoint.sh rename to docker/ee/ee-entrypoint.sh diff --git a/docker/ee/ee-production.Dockerfile b/docker/ee/ee-production.Dockerfile index b69458daa1..e611643f30 100644 --- a/docker/ee/ee-production.Dockerfile +++ b/docker/ee/ee-production.Dockerfile @@ -145,12 +145,13 @@ COPY --from=builder /app/frontend/build ./app/frontend/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/entrypoint.sh ./app/server/entrypoint.sh 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 ./docker/ee/ee-entrypoint.sh ./app/server/ee-entrypoint.sh + # Define non-sudo user RUN useradd --create-home --home-dir /home/appuser appuser \ && chown -R appuser:0 /app \ @@ -214,4 +215,4 @@ RUN npm install dotenv@10.0.0 joi@17.4.1 RUN npm cache clean --force -ENTRYPOINT ["./server/entrypoint.sh"] +ENTRYPOINT ["./server/ee-entrypoint.sh"] diff --git a/docker/ee/ee-try-entrypoint-lts.sh b/docker/ee/ee-try-entrypoint-lts.sh new file mode 100755 index 0000000000..27590534d0 --- /dev/null +++ b/docker/ee/ee-try-entrypoint-lts.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -e + +# Start Redis +# service redis-server start +# redis-server /etc/redis/redis.conf + +# Start Postgres +service postgresql start + +# Export the PORT variable to be used by the application +export PORT=${PORT:-80} + +# Start Supervisor +exec supervisord -c /etc/supervisor/conf.d/supervisord.conf diff --git a/server/try-entrypoint.sh b/docker/ee/ee-try-entrypoint.sh old mode 100644 new mode 100755 similarity index 96% rename from server/try-entrypoint.sh rename to docker/ee/ee-try-entrypoint.sh index 5843b49ffd..5143e10e75 --- a/server/try-entrypoint.sh +++ b/docker/ee/ee-try-entrypoint.sh @@ -22,7 +22,7 @@ echo "Starting Temporal Server..." export PORT=${PORT:-80} # Start Supervisor -/usr/bin/supervisord -n & +exec supervisord -c /etc/supervisor/conf.d/supervisord.conf & # Wait for Temporal Server to be ready echo "Waiting for Temporal Server to be ready..." diff --git a/docker/try-tooljet.Dockerfile b/docker/ee/ee-try-tooljet-lts.Dockerfile similarity index 57% rename from docker/try-tooljet.Dockerfile rename to docker/ee/ee-try-tooljet-lts.Dockerfile index 695f17b913..5eb10b938a 100644 --- a/docker/try-tooljet.Dockerfile +++ b/docker/ee/ee-try-tooljet-lts.Dockerfile @@ -1,21 +1,31 @@ -FROM tooljet/tooljet-ce:latest +FROM tooljet/tooljet:ee-lts-latest -# copy postgrest executable -COPY --from=postgrest/postgrest:v10.1.1.20221215 /bin/postgrest /bin +# Copy PostgREST executable +COPY --from=postgrest/postgrest:v12.2.0 /bin/postgrest /bin -# Install Postgres +# Install PostgreSQL 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 +RUN mkdir -p /var/log/supervisor /var/run/postgresql /var/lib/postgresql /var/lib/redis && \ + chown -R appuser:appuser /etc/supervisor /var/log/supervisor /var/lib/redis && \ + chown -R postgres:postgres /var/run/postgresql /var/lib/postgresql + +# Configure Supervisor to manage PostgREST, ToolJet, and Redis RUN echo "[supervisord] \n" \ "nodaemon=true \n" \ + "user=root \n" \ "\n" \ "[program:postgrest] \n" \ "command=/bin/postgrest \n" \ @@ -23,12 +33,23 @@ RUN echo "[supervisord] \n" \ "autorestart=true \n" \ "\n" \ "[program:tooljet] \n" \ + "user=appuser \n" \ "command=/bin/bash -c '/app/server/scripts/init-db-boot.sh' \n" \ "autostart=true \n" \ "autorestart=true \n" \ "stderr_logfile=/dev/stdout \n" \ "stderr_logfile_maxbytes=0 \n" \ "stdout_logfile=/dev/stdout \n" \ + "stdout_logfile_maxbytes=0 \n" \ + "\n" \ + "[program:redis] \n" \ + "user=appuser \n" \ + "command=/usr/bin/redis-server \n" \ + "autostart=true \n" \ + "autorestart=true \n" \ + "stderr_logfile=/dev/stdout \n" \ + "stderr_logfile_maxbytes=0 \n" \ + "stdout_logfile=/dev/stdout \n" \ "stdout_logfile_maxbytes=0 \n" | sed 's/ //' > /etc/supervisor/conf.d/supervisord.conf # ENV defaults @@ -49,10 +70,17 @@ ENV TOOLJET_HOST=http://localhost \ PGRST_HOST=http://localhost:3000 \ PGRST_DB_URI=postgres://tooljet:postgres@localhost/tooljet_db \ PGRST_JWT_SECRET=r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj \ + PGRST_DB_PRE_CONFIG=postgrest.pre_config \ ORM_LOGGING=true \ DEPLOYMENT_PLATFORM=docker:local \ HOME=/home/appuser \ + REDIS_HOST=localhost \ + REDIS_PORT=6379 \ + REDIS_USER=default \ + REDIS_PASS= \ TERM=xterm -# Prepare DB and start application -ENTRYPOINT service postgresql start 1> /dev/null && /usr/bin/supervisord +# Set the entrypoint +COPY ./docker/ee/ee-try-entrypoint-lts.sh /ee-try-entrypoint-lts.sh +RUN chmod +x /ee-try-entrypoint-lts +ENTRYPOINT ["/ee-try-entrypoint-lts.sh"] diff --git a/docker/ee/ee-try-tooljet.Dockerfile b/docker/ee/ee-try-tooljet.Dockerfile new file mode 100644 index 0000000000..11cbe88be3 --- /dev/null +++ b/docker/ee/ee-try-tooljet.Dockerfile @@ -0,0 +1,117 @@ +FROM tooljet/tooljet:ee-latest + +# Copy postgrest executable +COPY --from=postgrest/postgrest:v12.2.0 /bin/postgrest /bin + +# 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 + + +RUN apt update && apt -y install redis + +# Create appuser home & ensure permission for supervisord and services +RUN mkdir -p /var/log/supervisor /var/run/postgresql /var/lib/postgresql /var/lib/redis && \ + 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.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 + +# 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 + +# Copy Temporal configuration files +COPY ./docker/ee/temporal-server.yaml /etc/temporal/temporal-server.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 + +# Configure Supervisor to manage PostgREST, ToolJet, and Redis +RUN echo "[supervisord] \n" \ + "nodaemon=true \n" \ + "user=root \n" \ + "\n" \ + "[program:postgrest] \n" \ + "command=/bin/postgrest \n" \ + "autostart=true \n" \ + "autorestart=true \n" \ + "\n" \ + "[program:tooljet] \n" \ + "user=appuser \n" \ + "command=/bin/bash -c '/app/server/scripts/init-db-boot.sh' \n" \ + "autostart=true \n" \ + "autorestart=true \n" \ + "stderr_logfile=/dev/stdout \n" \ + "stderr_logfile_maxbytes=0 \n" \ + "stdout_logfile=/dev/stdout \n" \ + "stdout_logfile_maxbytes=0 \n" \ + "\n" \ + "[program:redis] \n" \ + "user=appuser \n" \ + "command=/usr/bin/redis-server \n" \ + "autostart=true \n" \ + "autorestart=true \n" \ + "stderr_logfile=/dev/stdout \n" \ + "stderr_logfile_maxbytes=0 \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 \ + PORT=80 \ + NODE_ENV=production \ + LOCKBOX_MASTER_KEY=replace_with_lockbox_master_key \ + SECRET_KEY_BASE=replace_with_secret_key_base \ + PG_DB=tooljet_production \ + PG_USER=tooljet \ + PG_PASS=postgres \ + PG_HOST=localhost \ + ENABLE_TOOLJET_DB=true \ + TOOLJET_DB_HOST=localhost \ + TOOLJET_DB_USER=tooljet \ + TOOLJET_DB_PASS=postgres \ + TOOLJET_DB=tooljet_db \ + PGRST_HOST=http://localhost:3000 \ + PGRST_DB_URI=postgres://tooljet:postgres@localhost/tooljet_db \ + PGRST_JWT_SECRET=r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj \ + PGRST_DB_PRE_CONFIG=postgrest.pre_config \ + ORM_LOGGING=true \ + DEPLOYMENT_PLATFORM=docker:local \ + HOME=/home/appuser \ + REDIS_HOST=localhost \ + REDIS_PORT=6379 \ + REDIS_USER=default \ + REDIS_PASS= \ + 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_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"] diff --git a/docker/ee/temporal-server.yaml b/docker/ee/temporal-server.yaml new file mode 100644 index 0000000000..bc17ed934f --- /dev/null +++ b/docker/ee/temporal-server.yaml @@ -0,0 +1,75 @@ +log: + stdout: true + level: info + +persistence: + defaultStore: sqlite-default + visibilityStore: sqlite-visibility + numHistoryShards: 4 + datastores: + sqlite-default: + sql: + pluginName: "sqlite" + databaseName: "/etc/temporal/default.db" + connectAddr: "localhost" + connectProtocol: "tcp" + connectAttributes: + cache: "private" + setup: true + + sqlite-visibility: + sql: + pluginName: "sqlite" + databaseName: "/etc/temporal/visibility.db" + connectAddr: "localhost" + connectProtocol: "tcp" + connectAttributes: + cache: "private" + setup: true + +global: + membership: + maxJoinDuration: 30s + broadcastAddress: "127.0.0.1" + pprof: + port: 7936 + +services: + frontend: + rpc: + grpcPort: 7233 + membershipPort: 6933 + bindOnLocalHost: true + httpPort: 7243 + + matching: + rpc: + grpcPort: 7235 + membershipPort: 6935 + bindOnLocalHost: true + + history: + rpc: + grpcPort: 7234 + membershipPort: 6934 + bindOnLocalHost: true + + worker: + rpc: + membershipPort: 6939 + +clusterMetadata: + enableGlobalNamespace: false + failoverVersionIncrement: 10 + masterClusterName: "active" + currentClusterName: "active" + clusterInformation: + active: + enabled: true + initialFailoverVersion: 1 + rpcName: "frontend" + rpcAddress: "localhost:7236" + httpAddress: "localhost:7243" + +dcRedirectionPolicy: + policy: "noop" diff --git a/docker/ee/temporal-ui-server.yaml b/docker/ee/temporal-ui-server.yaml new file mode 100644 index 0000000000..4daf530ae2 --- /dev/null +++ b/docker/ee/temporal-ui-server.yaml @@ -0,0 +1,8 @@ +temporalGrpcAddress: 127.0.0.1:7233 # Use the correct Temporal server address +host: 0.0.0.0 +port: 8080 +enableUi: true +cors: + allowOrigins: + - http://localhost:8080 +defaultNamespace: default diff --git a/frontend/ee b/frontend/ee index dcd948d284..a1435b3b0e 160000 --- a/frontend/ee +++ b/frontend/ee @@ -1 +1 @@ -Subproject commit dcd948d284b5f14a868480830e09b90496db8572 +Subproject commit a1435b3b0e66a0c731812256d3d495d5bf48d5bb diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx b/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx index 7043c78774..5ece3710de 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx +++ b/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx @@ -22,6 +22,8 @@ import { handleActivateTargets, handleDeactivateTargets, handleActivateNonDraggingComponents, + computeScrollDelta, + computeScrollDeltaOnDrag, } from './gridUtils'; import { dragContextBuilder, getAdjustedDropPosition } from './helpers/dragEnd'; import useStore from '@/AppBuilder/_stores/store'; @@ -56,6 +58,7 @@ export default function Grid({ gridWidth, currentLayout }) { const canvasWidth = NO_OF_GRIDS * gridWidth; const getHoveredComponentForGrid = useStore((state) => state.getHoveredComponentForGrid, shallow); const getResolvedComponent = useStore((state) => state.getResolvedComponent, shallow); + const updateContainerAutoHeight = useStore((state) => state.updateContainerAutoHeight, shallow); const [canvasBounds, setCanvasBounds] = useState(CANVAS_BOUNDS); const draggingComponentId = useStore((state) => state.draggingComponentId, shallow); const resizingComponentId = useGridStore((state) => state.resizingComponentId, shallow); @@ -345,6 +348,7 @@ export default function Grid({ gridWidth, currentLayout }) { const handleDragEnd = useCallback( (boxPositions) => { let newParent = null; + let oldParent = null; const updatedLayouts = boxPositions.reduce((layouts, { id, x, y, parent }) => { const currentWidget = boxList.find((box) => box.id === id); const containerWidth = parent ? useGridStore.getState().subContainerWidths[parent] : gridWidth; @@ -389,7 +393,7 @@ export default function Grid({ gridWidth, currentLayout }) { } } newParent = parent ? parent : null; - + oldParent = currentWidget.component?.parent; layouts[id] = { width: _width, height: _height, @@ -400,6 +404,11 @@ export default function Grid({ gridWidth, currentLayout }) { return layouts; }, {}); setComponentLayout(updatedLayouts, newParent, undefined, { updateParent: true }); + + // const currentWidget = boxList.find((box) => box.id === id); + updateContainerAutoHeight(newParent); + updateContainerAutoHeight(oldParent); + toggleCanvasUpdater(); }, // eslint-disable-next-line react-hooks/exhaustive-deps @@ -579,6 +588,11 @@ export default function Grid({ gridWidth, currentLayout }) { keepRatio={false} individualGroupableProps={individualGroupableProps} onResize={(e) => { + if(resizingComponentId !== e.target.id) { + useGridStore.getState().actions.setResizingComponentId(e.target.id); + showGridLines(); + } + const currentWidget = boxList.find(({ id }) => id === e.target.id); let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth; if (currentWidget.component?.parent) { @@ -639,9 +653,7 @@ export default function Grid({ gridWidth, currentLayout }) { return false; } handleActivateNonDraggingComponents(); - useGridStore.getState().actions.setResizingComponentId(e.target.id); e.setMin([gridWidth, GRID_HEIGHT]); - showGridLines(); }} onResizeEnd={(e) => { try { @@ -867,20 +879,19 @@ export default function Grid({ gridWidth, currentLayout }) { const targetSlotId = target?.slotId; const targetGridWidth = useGridStore.getState().subContainerWidths[targetSlotId] || gridWidth; - - // const restrictedWidgets = RESTRICTED_WIDGETS_CONFIG?.[source.widgetType] || []; - // const draggedWidgetType = dragged.widgetType; const isParentChangeAllowed = dragContext.isDroppable; // Compute new position let { left, top } = getAdjustedDropPosition(e, target, isParentChangeAllowed, targetGridWidth, dragged); const isModalToCanvas = source.isModal && target.slotId === 'real-canvas'; + let scrollDelta = computeScrollDelta({ source }); if (isParentChangeAllowed && !isModalToCanvas) { - const parent = target.slotId === 'real-canvas' ? null : target.slotId; // Special case for Modal; If source widget is modal, prevent drops to canvas - handleDragEnd([{ id: e.target.id, x: left, y: top, parent }]); + const parent = target.slotId === 'real-canvas' ? null : target.slotId; + + handleDragEnd([{ id: e.target.id, x: left, y: top + scrollDelta, parent }]); } else { const sourcegridWidth = useGridStore.getState().subContainerWidths[source.slotId] || gridWidth; @@ -889,9 +900,8 @@ export default function Grid({ gridWidth, currentLayout }) { !isModalToCanvas ?? toast.error(`${dragged.widgetType} is not compatible as a child component of ${target.widgetType}`); } - // Apply transform for smooth transition - e.target.style.transform = `translate(${left}px, ${top}px)`; + e.target.style.transform = `translate(${left}px, ${top + scrollDelta}px)`; // Force reordering of conatiner if the parent has not changed const newParentId = target.slotId === 'real-canvas' ? 'canvas' : target.slotId; @@ -945,7 +955,7 @@ export default function Grid({ gridWidth, currentLayout }) { const isParentModal = isParentNewModal || isParentLegacyModal || isParentModalSlot; if (isParentModal) { - const modalContainer = e.target.closest('.tj-modal-widget-content'); + const modalContainer = e.target.closest('.tj-modal--container'); const mainCanvas = document.getElementById('real-canvas'); const mainRect = mainCanvas.getBoundingClientRect(); @@ -959,12 +969,6 @@ export default function Grid({ gridWidth, currentLayout }) { setCanvasBounds({ ...relativePosition }); } - e.target.style.transform = `translate(${left}px, ${top}px)`; - e.target.setAttribute( - 'widget-pos2', - `translate: ${e.translate[0]} | Round: ${Math.round(e.translate[0] / gridWidth) * gridWidth} | ${gridWidth}` - ); - // This block is to show grid lines on the canvas when the dragged element is over a new canvas if (document.elementFromPoint(e.clientX, e.clientY)) { const targetElems = document.elementsFromPoint(e.clientX, e.clientY); @@ -992,6 +996,17 @@ export default function Grid({ gridWidth, currentLayout }) { handleActivateTargets(newParentId); } } + + // Build the drag context from the event + const source = { slotId: oldParentId }; + let scrollDelta = computeScrollDeltaOnDrag({ source }); + + e.target.style.transform = `translate(${left}px, ${top - scrollDelta}px)`; + e.target.setAttribute( + 'widget-pos2', + `translate: ${e.translate[0]} | Round: ${Math.round(e.translate[0] / gridWidth) * gridWidth} | ${gridWidth}` + ); + // Postion ghost element exactly as same at dragged element if (document.getElementById(`moveable-drag-ghost`)) { document.getElementById(`moveable-drag-ghost`).style.transform = `translate(${left}px, ${top}px)`; @@ -1081,6 +1096,7 @@ export default function Grid({ gridWidth, currentLayout }) { } }} snapGridAll={true} + scrollable={true} /> > ); diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js b/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js index da179bc11d..f36f921be9 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js +++ b/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js @@ -415,6 +415,20 @@ export function hideGridLines() { document.getElementById('real-canvas')?.classList.add('hide-grid'); } +export function showGridLinesOnSlot(slotId) { + var canvasElm = document.getElementById(`canvas-${slotId}`); + + canvasElm.classList.remove('hide-grid'); + canvasElm.classList.add('show-grid'); +} + +export function hideGridLinesOnSlot(slotId) { + var canvasElm = document.getElementById(`canvas-${slotId}`); + + canvasElm.classList.remove('show-grid'); + canvasElm.classList.add('hide-grid'); +} + // Track previously active elements for efficient cleanup let previousActiveWidgets = null; let previousActiveCanvas = null; @@ -488,3 +502,18 @@ export const handleDeactivateTargets = () => { component.classList.remove('non-dragging-component'); }); }; +export const computeScrollDelta = ({ source }) => { + // Only need to calculate scroll delta when moving from a sub-container + if (source.slotId !== 'real-canvas') { + const subContainerWrap = document + .querySelector(`#canvas-${source.slotId}`) + ?.closest('.sub-container-overflow-wrap'); + + return subContainerWrap?.scrollTop || 0; + } + + // Default case: No scroll adjustment needed + return 0; +}; + +export const computeScrollDeltaOnDrag = computeScrollDelta; diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js b/frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js index a9405d043e..da5a8341bf 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js +++ b/frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js @@ -175,7 +175,6 @@ export class DragContext { const restrictedWidgets = [...restrictedWidgetsOnTarget, ...restrictedWidgetsOnTargetSlot]; return !restrictedWidgets.includes(dragged.widgetType); - ß; } } diff --git a/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx b/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx index 9507370e13..6610ae5fb4 100644 --- a/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx +++ b/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx @@ -33,6 +33,7 @@ const SHOULD_ADD_BOX_SHADOW_AND_VISIBILITY = [ 'Divider', 'VerticalDivider', 'Link', + 'Form', ]; const RenderWidget = ({ diff --git a/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx b/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx index ef5a5dbd7d..cbebcb0425 100644 --- a/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx +++ b/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx @@ -22,6 +22,7 @@ import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; import { search, searchKeymap, searchPanelOpen } from '@codemirror/search'; import { handleSearchPanel, SearchBtn } from './SearchBox'; +import { useQueryPanelKeyHooks } from './useQueryPanelKeyHooks'; import { isInsideParent } from './utils'; const langSupport = Object.freeze({ @@ -73,6 +74,8 @@ const MultiLineCodeEditor = (props) => { const [editorView, setEditorView] = React.useState(null); + const { queryPanelKeybindings } = useQueryPanelKeyHooks(onChange, currentValueRef, 'multiline'); + const handleOnBlur = () => { if (!delayOnChange) return onChange(currentValueRef.current); setTimeout(() => { @@ -94,6 +97,7 @@ const MultiLineCodeEditor = (props) => { highlightActiveLine: false, autocompletion: hideSuggestion ?? true, highlightActiveLineGutter: false, + defaultKeymap: false, completionKeymap: true, searchKeymap: false, }; @@ -203,7 +207,12 @@ const MultiLineCodeEditor = (props) => { }; } - const customKeyMaps = [...defaultKeymap, ...completionKeymap, ...searchKeymap]; + const customKeyMaps = [ + ...defaultKeymap.filter((keyBinding) => keyBinding.key !== 'Mod-Enter'), // Remove default keybinding for Mod-Enter + ...completionKeymap, + ...searchKeymap, + ]; + const customTabKeymap = keymap.of([ { key: 'Tab', @@ -224,6 +233,7 @@ const MultiLineCodeEditor = (props) => { return true; }, }, + ...queryPanelKeybindings, ]); // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx b/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx index 7c9ed53158..65e6f2eadd 100644 --- a/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx +++ b/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx @@ -22,6 +22,7 @@ import CodeHinter from './CodeHinter'; import { removeNestedDoubleCurlyBraces } from '@/_helpers/utils'; import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; +import { useQueryPanelKeyHooks } from './useQueryPanelKeyHooks'; const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...restProps }) => { const { initialValue, onChange, enablePreview = true, portalProps } = restProps; @@ -201,6 +202,8 @@ const EditorInput = ({ const getServerSideGlobalSuggestions = useStore((state) => state.getServerSideGlobalSuggestions, shallow); const getSuggestions = useStore((state) => state.getSuggestions, shallow); + const { queryPanelKeybindings } = useQueryPanelKeyHooks(onBlurUpdate, currentValue, 'singleline'); + const isInsideQueryManager = useMemo( () => isInsideParent(wrapperRef?.current, 'query-manager'), [wrapperRef.current] @@ -271,7 +274,10 @@ const EditorInput = ({ maxRenderedOptions: 10, }); - const customKeyMaps = [...defaultKeymap, ...completionKeymap]; + const customKeyMaps = [ + ...defaultKeymap.filter((keyBinding) => keyBinding.key !== 'Mod-Enter'), // Remove default keybinding for Mod-Enter + ...completionKeymap, + ]; const customTabKeymap = keymap.of([ { key: 'Tab', @@ -293,6 +299,7 @@ const EditorInput = ({ } }, }, + ...queryPanelKeybindings, ]); const handleOnChange = React.useCallback((val) => { @@ -442,7 +449,8 @@ const EditorInput = ({ bracketMatching: true, foldGutter: false, highlightActiveLine: false, - autocompletion: showSuggestions, + autocompletion: true, + defaultKeymap: false, completionKeymap: true, searchKeymap: false, }} diff --git a/frontend/src/AppBuilder/CodeEditor/styles.scss b/frontend/src/AppBuilder/CodeEditor/styles.scss index d7f715fb3b..d0a328bae0 100644 --- a/frontend/src/AppBuilder/CodeEditor/styles.scss +++ b/frontend/src/AppBuilder/CodeEditor/styles.scss @@ -220,6 +220,9 @@ .query-hinter{ flex-grow: 1; } + .cm-editor { + min-height: 150px !important; + } } .code-editor-query-panel{ &.show-line-numbers{ @@ -398,6 +401,12 @@ } } +.rest-api-body-codehinter { + .cm-editor { + min-height: 150px !important; + } +} + .border-danger { .cm-editor { border: 1px solid red !important; diff --git a/frontend/src/AppBuilder/CodeEditor/useQueryPanelKeyHooks.js b/frontend/src/AppBuilder/CodeEditor/useQueryPanelKeyHooks.js new file mode 100644 index 0000000000..1a41a7f19b --- /dev/null +++ b/frontend/src/AppBuilder/CodeEditor/useQueryPanelKeyHooks.js @@ -0,0 +1,58 @@ +import { useModuleId } from '@/AppBuilder/_contexts/ModuleContext'; +import useStore from '@/AppBuilder/_stores/store'; +import { useCallback, useEffect, useState } from 'react'; +import { useLocation } from 'react-router-dom'; + +export const useQueryPanelKeyHooks = (onChange, value, type) => { + const queryPanelHeight = useStore((state) => state.queryPanel.queryPanelHeight); + const runQueryOnShortcut = useStore((state) => state.queryPanel.runQueryOnShortcut); + const previewQueryOnShortcut = useStore((state) => state.queryPanel.previewQueryOnShortcut); + const moduleId = useModuleId(); + const location = useLocation(); + const { pathname } = location; + + const [queryPanelKeybindings, setQueryPanelKeybindings] = useState([]); + + const handleRunQuery = useCallback( + (view) => { + const isEditor = pathname.includes('/apps/'); + if (queryPanelHeight !== 0 && isEditor) { + onChange(type === 'multiline' ? value.current : value); + runQueryOnShortcut(); + } + return true; + }, + [queryPanelHeight, onChange, runQueryOnShortcut, value] + ); + + const handlePreviewQuery = useCallback( + (view) => { + const isEditor = pathname.includes('/apps/'); + if (queryPanelHeight !== 0 && isEditor) { + onChange(type === 'multiline' ? value.current : value); + previewQueryOnShortcut(moduleId); + } + return true; + }, + [queryPanelHeight, moduleId, onChange, previewQueryOnShortcut, value] + ); + + useEffect(() => { + setQueryPanelKeybindings([ + { + key: 'Mod-Enter', + preventDefault: true, + run: handleRunQuery, + }, + { + key: 'Mod-Shift-Enter', + preventDefault: true, + run: handlePreviewQuery, + }, + ]); + }, [handleRunQuery, handlePreviewQuery]); + + return { + queryPanelKeybindings, + }; +}; diff --git a/frontend/src/AppBuilder/QueryManager/Components/QueryManagerBody.jsx b/frontend/src/AppBuilder/QueryManager/Components/QueryManagerBody.jsx index 9e6737f41c..244d8e53cf 100644 --- a/frontend/src/AppBuilder/QueryManager/Components/QueryManagerBody.jsx +++ b/frontend/src/AppBuilder/QueryManager/Components/QueryManagerBody.jsx @@ -381,7 +381,6 @@ export const BaseQueryManagerBody = ({ darkMode, activeTab, renderCopilot = () = {activeTab === 1 && renderQueryElement()} {activeTab === 2 && renderTransformation()} {activeTab === 3 && renderQueryOptions()} -