Merge branch 'main' into appdefinition-architecture-revamp
|
|
@ -1,4 +1,4 @@
|
|||
name: Cypress E2E Test
|
||||
name: Cypress App-Builder
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
|
|
@ -117,94 +117,80 @@ jobs:
|
|||
name: screenshots
|
||||
path: cypress-tests/cypress/screenshots
|
||||
|
||||
Cypress-Platform:
|
||||
Cypress-App-builder-Subpath:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
if: ${{ github.event.action == 'labeled' && github.event.label.name == 'run-cypress-workspace' }}
|
||||
if: ${{ github.event.action == 'labeled' && github.event.label.name == 'run-cypress-app-builder-subpath' }}
|
||||
|
||||
steps:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18.3.0
|
||||
|
||||
- name: Set up Docker
|
||||
uses: docker-practice/actions-setup-docker@master
|
||||
|
||||
- name: Run PosgtreSQL Database Docker Container
|
||||
run: |
|
||||
sudo docker network create tooljet
|
||||
sudo docker run -d --name postgres -p 5432:5432 -e POSTGRES_PASSWORD=postgres -e POSTGRES_USER=postgres -e POSTGRES_PORT=5432 -d postgres:13
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
- name: Install and build dependencies
|
||||
# Create Docker Buildx builder with platform configuration
|
||||
- name: Set up Docker Buildx
|
||||
run: |
|
||||
npm cache clean --force
|
||||
npm install
|
||||
npm install --prefix server
|
||||
npm install --prefix frontend
|
||||
npm run build:plugins
|
||||
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: Build docker image
|
||||
run: docker buildx build --platform=linux/amd64 -f docker/production.Dockerfile . -t tooljet/tj-osv:cypress
|
||||
|
||||
- name: Set up environment variables
|
||||
run: |
|
||||
echo "TOOLJET_HOST=http://localhost:8082" >> .env
|
||||
echo "TOOLJET_HOST=http://localhost:3000" >> .env
|
||||
echo "LOCKBOX_MASTER_KEY=cd97331a419c09387bef49787f7da8d2a81d30733f0de6bed23ad8356d2068b2" >> .env
|
||||
echo "SECRET_KEY_BASE=7073b9a35a15dd20914ae17e36a693093f25b74b96517a5fec461fc901c51e011cd142c731bee48c5081ec8bac321c1f259ef097ef2a16f25df17a3798c03426" >> .env
|
||||
echo "PG_DB=tooljet_development" >> .env
|
||||
echo "PG_USER=postgres" >> .env
|
||||
echo "PG_HOST=localhost" >> .env
|
||||
echo "PG_HOST=postgres" >> .env
|
||||
echo "PG_PASS=postgres" >> .env
|
||||
echo "PG_PORT=5432" >> .env
|
||||
echo "ENABLE_TOOLJET_DB=true" >> .env
|
||||
echo "TOOLJET_DB=tooljet" >> .env
|
||||
echo "TOOLJET_DB=tooljet_db" >> .env
|
||||
echo "TOOLJET_DB_USER=postgres" >> .env
|
||||
echo "TOOLJET_DB_HOST=localhost" >> .env
|
||||
echo "TOOLJET_DB_HOST=postgres" >> .env
|
||||
echo "TOOLJET_DB_PASS=postgres" >> .env
|
||||
echo "PGRST_JWT_SECRET=r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj" >> .env
|
||||
echo "PGRST_HOST=localhost:3001" >> .env
|
||||
echo "PGRST_HOST=postgrest" >> .env
|
||||
echo "PGRST_DB_URI=postgres://postgres:postgres@postgres/tooljet_db" >> .env
|
||||
echo "SSO_GIT_OAUTH2_CLIENT_ID=dummy" >> .env
|
||||
echo "SSO_GIT_OAUTH2_CLIENT_SECRET=dummy" >> .env
|
||||
echo "SSO_GIT_OAUTH2_HOST=dummy" >> .env
|
||||
echo "SSO_GOOGLE_OAUTH2_CLIENT_ID=dummy" >> .env
|
||||
echo "SUB_PATH=/apps/tooljet/" >> .env
|
||||
echo "NODE_ENV=production" >> .env
|
||||
echo "SERVE_CLIENT=true" >> .env
|
||||
|
||||
- name: Set up database
|
||||
run: |
|
||||
npm run --prefix server db:create
|
||||
npm run --prefix server db:reset
|
||||
npm run --prefix server db:seed
|
||||
- name: Pulling the docker-compose file
|
||||
run: curl -LO https://tooljet-test.s3.us-west-1.amazonaws.com/docker-compose.yaml && mkdir postgres_data
|
||||
|
||||
- name: sleep 5
|
||||
run: sleep 5
|
||||
- name: Run docker-compose file
|
||||
run: docker-compose up -d
|
||||
|
||||
- name: Run PostgREST Docker Container
|
||||
run: |
|
||||
sudo docker run -d --name postgrest --network tooljet -p 3001:3000 \
|
||||
-e PGRST_DB_URI="postgres://postgres:postgres@postgres:5432/tooljet" -e PGRST_DB_ANON_ROLE="postgres" -e PGRST_JWT_SECRET="r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj" \
|
||||
postgrest/postgrest:v10.1.1.20221215
|
||||
- name: Checking containers
|
||||
run: docker ps -a
|
||||
|
||||
- name: Run plugins compilation in watch mode
|
||||
run: cd plugins && npm start &
|
||||
|
||||
- name: Run the server
|
||||
run: cd server && npm run start:dev &
|
||||
|
||||
- name: Run the client
|
||||
run: cd frontend && npm start &
|
||||
- name: docker logs
|
||||
run: sudo docker logs Tooljet-app
|
||||
|
||||
- name: Wait for the server to be ready
|
||||
run: |
|
||||
timeout 1500 bash -c '
|
||||
until curl --silent --fail http://localhost:8082; do
|
||||
until curl --silent --fail http://localhost:80/apps/tooljet/; do
|
||||
sleep 5
|
||||
done'
|
||||
|
||||
- name: docker logs
|
||||
run: sudo docker logs postgrest
|
||||
|
||||
- name: Create Cypress environment file
|
||||
id: create-json
|
||||
uses: jsdaniell/create-json@1.1.2
|
||||
|
|
@ -213,57 +199,12 @@ jobs:
|
|||
json: ${{ secrets.CYPRESS_SECRETS }}
|
||||
dir: "./cypress-tests"
|
||||
|
||||
- name: Platform
|
||||
- name: App Builder subpath
|
||||
uses: cypress-io/github-action@v5
|
||||
with:
|
||||
working-directory: ./cypress-tests
|
||||
config: "baseUrl=http://localhost:8082"
|
||||
config-file: cypress-workspace.config.js
|
||||
|
||||
- name: Capture Screenshots
|
||||
uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: screenshots
|
||||
path: cypress-tests/cypress/screenshots
|
||||
|
||||
Cypress-Marketplace:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
if: ${{ github.event.action == 'labeled' && github.event.label.name == 'run-cypress-marketplace' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18.3.0
|
||||
|
||||
- name: Checking the PR URL
|
||||
run: |
|
||||
timeout 1500 bash -c '
|
||||
until curl --silent --fail https://tooljet-pr-cypress-${{ env.PR_NUMBER }}.onrender.com; do
|
||||
sleep 100
|
||||
done'
|
||||
|
||||
- name: Create Cypress environment file
|
||||
id: create-json
|
||||
uses: jsdaniell/create-json@1.1.2
|
||||
with:
|
||||
name: "cypress.env.json"
|
||||
json: ${{ secrets.CYPRESS_SECRETS }}
|
||||
dir: "./cypress-tests"
|
||||
|
||||
- name: Marketplace
|
||||
uses: cypress-io/github-action@v5
|
||||
with:
|
||||
working-directory: ./cypress-tests
|
||||
config: "baseUrl=https://tooljet-pr-cypress-${{ env.PR_NUMBER }}.onrender.com"
|
||||
config-file: cypress-marketplace.config.js
|
||||
config: "baseUrl=http://localhost:80/apps/tooljet/"
|
||||
config-file: cypress-app-builder.config.js
|
||||
|
||||
- name: Capture Screenshots
|
||||
uses: actions/upload-artifact@v3
|
||||
201
.github/workflows/cypress-marketplace.yml
vendored
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
name: Cypress Marketplace
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [labeled, unlabeled, closed]
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
||||
|
||||
jobs:
|
||||
Cypress-Marketplace:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
if: ${{ github.event.action == 'labeled' && github.event.label.name == 'run-cypress-marketplace' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
# 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: Build docker image
|
||||
run: docker buildx build --platform=linux/amd64 -f docker/production.Dockerfile . -t tooljet/tj-osv:cypress
|
||||
|
||||
- name: Set up environment variables
|
||||
run: |
|
||||
echo "TOOLJET_HOST=http://localhost:3000" >> .env
|
||||
echo "LOCKBOX_MASTER_KEY=cd97331a419c09387bef49787f7da8d2a81d30733f0de6bed23ad8356d2068b2" >> .env
|
||||
echo "SECRET_KEY_BASE=7073b9a35a15dd20914ae17e36a693093f25b74b96517a5fec461fc901c51e011cd142c731bee48c5081ec8bac321c1f259ef097ef2a16f25df17a3798c03426" >> .env
|
||||
echo "PG_DB=tooljet_development" >> .env
|
||||
echo "PG_USER=postgres" >> .env
|
||||
echo "PG_HOST=postgres" >> .env
|
||||
echo "PG_PASS=postgres" >> .env
|
||||
echo "PG_PORT=5432" >> .env
|
||||
echo "ENABLE_TOOLJET_DB=true" >> .env
|
||||
echo "TOOLJET_DB=tooljet_db" >> .env
|
||||
echo "TOOLJET_DB_USER=postgres" >> .env
|
||||
echo "TOOLJET_DB_HOST=postgres" >> .env
|
||||
echo "TOOLJET_DB_PASS=postgres" >> .env
|
||||
echo "PGRST_JWT_SECRET=r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj" >> .env
|
||||
echo "PGRST_HOST=postgrest" >> .env
|
||||
echo "PGRST_DB_URI=postgres://postgres:postgres@postgres/tooljet_db" >> .env
|
||||
echo "SSO_GIT_OAUTH2_CLIENT_ID=dummy" >> .env
|
||||
echo "SSO_GIT_OAUTH2_CLIENT_SECRET=dummy" >> .env
|
||||
echo "SSO_GIT_OAUTH2_HOST=dummy" >> .env
|
||||
echo "SSO_GOOGLE_OAUTH2_CLIENT_ID=dummy" >> .env
|
||||
|
||||
- name: Pulling the docker-compose file
|
||||
run: curl -LO https://tooljet-test.s3.us-west-1.amazonaws.com/docker-compose.yaml && mkdir postgres_data
|
||||
|
||||
- name: Run docker-compose file
|
||||
run: docker-compose up -d
|
||||
|
||||
- name: Checking containers
|
||||
run: docker ps -a
|
||||
|
||||
- name: docker logs
|
||||
run: sudo docker logs Tooljet-app
|
||||
|
||||
- name: Wait for the server to be ready
|
||||
run: |
|
||||
timeout 1500 bash -c '
|
||||
until curl --silent --fail http://localhost:80; do
|
||||
sleep 5
|
||||
done'
|
||||
|
||||
- name: Create Cypress environment file
|
||||
id: create-json
|
||||
uses: jsdaniell/create-json@1.1.2
|
||||
with:
|
||||
name: "cypress.env.json"
|
||||
json: ${{ secrets.CYPRESS_SECRETS }}
|
||||
dir: "./cypress-tests"
|
||||
|
||||
- name: Marketplace
|
||||
uses: cypress-io/github-action@v5
|
||||
with:
|
||||
working-directory: ./cypress-tests
|
||||
config: "baseUrl=http://localhost:80"
|
||||
config-file: cypress-marketplace.config.js
|
||||
|
||||
- name: Capture Screenshots
|
||||
uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: screenshots
|
||||
path: cypress-tests/cypress/screenshots
|
||||
|
||||
Cypress-Marketplace-Subpath:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
if: ${{ github.event.action == 'labeled' && github.event.label.name == 'run-cypress-marketplace-subpath' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
# 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: Build docker image
|
||||
run: docker buildx build --platform=linux/amd64 -f docker/production.Dockerfile . -t tooljet/tj-osv:cypress
|
||||
|
||||
- name: Set up environment variables
|
||||
run: |
|
||||
echo "TOOLJET_HOST=http://localhost:3000" >> .env
|
||||
echo "LOCKBOX_MASTER_KEY=cd97331a419c09387bef49787f7da8d2a81d30733f0de6bed23ad8356d2068b2" >> .env
|
||||
echo "SECRET_KEY_BASE=7073b9a35a15dd20914ae17e36a693093f25b74b96517a5fec461fc901c51e011cd142c731bee48c5081ec8bac321c1f259ef097ef2a16f25df17a3798c03426" >> .env
|
||||
echo "PG_DB=tooljet_development" >> .env
|
||||
echo "PG_USER=postgres" >> .env
|
||||
echo "PG_HOST=postgres" >> .env
|
||||
echo "PG_PASS=postgres" >> .env
|
||||
echo "PG_PORT=5432" >> .env
|
||||
echo "ENABLE_TOOLJET_DB=true" >> .env
|
||||
echo "TOOLJET_DB=tooljet_db" >> .env
|
||||
echo "TOOLJET_DB_USER=postgres" >> .env
|
||||
echo "TOOLJET_DB_HOST=postgres" >> .env
|
||||
echo "TOOLJET_DB_PASS=postgres" >> .env
|
||||
echo "PGRST_JWT_SECRET=r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj" >> .env
|
||||
echo "PGRST_HOST=postgrest" >> .env
|
||||
echo "PGRST_DB_URI=postgres://postgres:postgres@postgres/tooljet_db" >> .env
|
||||
echo "SSO_GIT_OAUTH2_CLIENT_ID=dummy" >> .env
|
||||
echo "SSO_GIT_OAUTH2_CLIENT_SECRET=dummy" >> .env
|
||||
echo "SSO_GIT_OAUTH2_HOST=dummy" >> .env
|
||||
echo "SSO_GOOGLE_OAUTH2_CLIENT_ID=dummy" >> .env
|
||||
echo "SUB_PATH=/apps/tooljet/" >> .env
|
||||
echo "NODE_ENV=production" >> .env
|
||||
echo "SERVE_CLIENT=true" >> .env
|
||||
|
||||
- name: Pulling the docker-compose file
|
||||
run: curl -LO https://tooljet-test.s3.us-west-1.amazonaws.com/docker-compose.yaml && mkdir postgres_data
|
||||
|
||||
- name: Run docker-compose file
|
||||
run: docker-compose up -d
|
||||
|
||||
- name: Checking containers
|
||||
run: docker ps -a
|
||||
|
||||
- name: docker logs
|
||||
run: sudo docker logs Tooljet-app
|
||||
|
||||
- name: Wait for the server to be ready
|
||||
run: |
|
||||
timeout 1500 bash -c '
|
||||
until curl --silent --fail http://localhost:80/apps/tooljet/; do
|
||||
sleep 5
|
||||
done'
|
||||
|
||||
- name: Create Cypress environment file
|
||||
id: create-json
|
||||
uses: jsdaniell/create-json@1.1.2
|
||||
with:
|
||||
name: "cypress.env.json"
|
||||
json: ${{ secrets.CYPRESS_SECRETS }}
|
||||
dir: "./cypress-tests"
|
||||
|
||||
- name: Marketplace subpath
|
||||
uses: cypress-io/github-action@v5
|
||||
with:
|
||||
working-directory: ./cypress-tests
|
||||
config: "baseUrl=http://localhost:80/apps/tooljet/"
|
||||
config-file: cypress-marketplace.config.js
|
||||
|
||||
- name: Capture Screenshots
|
||||
uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: screenshots
|
||||
path: cypress-tests/cypress/screenshots
|
||||
218
.github/workflows/cypress-platform.yml
vendored
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
name: Cypress Platform
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [labeled, unlabeled, closed]
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
||||
|
||||
jobs:
|
||||
Cypress-Platform:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
if: ${{ github.event.action == 'labeled' && github.event.label.name == 'run-cypress-workspace' }}
|
||||
|
||||
steps:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18.3.0
|
||||
|
||||
- name: Set up Docker
|
||||
uses: docker-practice/actions-setup-docker@master
|
||||
|
||||
- name: Run PosgtreSQL Database Docker Container
|
||||
run: |
|
||||
sudo docker network create tooljet
|
||||
sudo docker run -d --name postgres -p 5432:5432 -e POSTGRES_PASSWORD=postgres -e POSTGRES_USER=postgres -e POSTGRES_PORT=5432 -d postgres:13
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
- name: Install and build dependencies
|
||||
run: |
|
||||
npm cache clean --force
|
||||
npm install
|
||||
npm install --prefix server
|
||||
npm install --prefix frontend
|
||||
npm run build:plugins
|
||||
|
||||
- name: Set up environment variables
|
||||
run: |
|
||||
echo "TOOLJET_HOST=http://localhost:8082" >> .env
|
||||
echo "LOCKBOX_MASTER_KEY=cd97331a419c09387bef49787f7da8d2a81d30733f0de6bed23ad8356d2068b2" >> .env
|
||||
echo "SECRET_KEY_BASE=7073b9a35a15dd20914ae17e36a693093f25b74b96517a5fec461fc901c51e011cd142c731bee48c5081ec8bac321c1f259ef097ef2a16f25df17a3798c03426" >> .env
|
||||
echo "PG_DB=tooljet_development" >> .env
|
||||
echo "PG_USER=postgres" >> .env
|
||||
echo "PG_HOST=localhost" >> .env
|
||||
echo "PG_PASS=postgres" >> .env
|
||||
echo "PG_PORT=5432" >> .env
|
||||
echo "ENABLE_TOOLJET_DB=true" >> .env
|
||||
echo "TOOLJET_DB=tooljet" >> .env
|
||||
echo "TOOLJET_DB_USER=postgres" >> .env
|
||||
echo "TOOLJET_DB_HOST=localhost" >> .env
|
||||
echo "TOOLJET_DB_PASS=postgres" >> .env
|
||||
echo "PGRST_JWT_SECRET=r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj" >> .env
|
||||
echo "PGRST_HOST=localhost:3001" >> .env
|
||||
echo "SSO_GIT_OAUTH2_CLIENT_ID=dummy" >> .env
|
||||
echo "SSO_GIT_OAUTH2_CLIENT_SECRET=dummy" >> .env
|
||||
echo "SSO_GIT_OAUTH2_HOST=dummy" >> .env
|
||||
echo "SSO_GOOGLE_OAUTH2_CLIENT_ID=dummy" >> .env
|
||||
|
||||
- name: Set up database
|
||||
run: |
|
||||
npm run --prefix server db:create
|
||||
npm run --prefix server db:reset
|
||||
npm run --prefix server db:seed
|
||||
|
||||
- name: sleep 5
|
||||
run: sleep 5
|
||||
|
||||
- name: Run PostgREST Docker Container
|
||||
run: |
|
||||
sudo docker run -d --name postgrest --network tooljet -p 3001:3000 \
|
||||
-e PGRST_DB_URI="postgres://postgres:postgres@postgres:5432/tooljet" -e PGRST_DB_ANON_ROLE="postgres" -e PGRST_JWT_SECRET="r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj" \
|
||||
postgrest/postgrest:v10.1.1.20221215
|
||||
|
||||
- name: Run plugins compilation in watch mode
|
||||
run: cd plugins && npm start &
|
||||
|
||||
- name: Run the server
|
||||
run: cd server && npm run start:dev &
|
||||
|
||||
- name: Run the client
|
||||
run: cd frontend && npm start &
|
||||
|
||||
- name: Wait for the server to be ready
|
||||
run: |
|
||||
timeout 1500 bash -c '
|
||||
until curl --silent --fail http://localhost:8082; do
|
||||
sleep 5
|
||||
done'
|
||||
|
||||
- name: docker logs
|
||||
run: sudo docker logs postgrest
|
||||
|
||||
- name: Create Cypress environment file
|
||||
id: create-json
|
||||
uses: jsdaniell/create-json@1.1.2
|
||||
with:
|
||||
name: "cypress.env.json"
|
||||
json: ${{ secrets.CYPRESS_SECRETS }}
|
||||
dir: "./cypress-tests"
|
||||
|
||||
- name: Platform
|
||||
uses: cypress-io/github-action@v5
|
||||
with:
|
||||
working-directory: ./cypress-tests
|
||||
config: "baseUrl=http://localhost:8082"
|
||||
config-file: cypress-workspace.config.js
|
||||
|
||||
- name: Capture Screenshots
|
||||
uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: screenshots
|
||||
path: cypress-tests/cypress/screenshots
|
||||
|
||||
Cypress-Platform-subpath:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
if: ${{ github.event.action == 'labeled' && github.event.label.name == 'run-cypress-workspace-subpath' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
# 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: Build docker image
|
||||
run: docker buildx build --platform=linux/amd64 -f docker/production.Dockerfile . -t tooljet/tj-osv:cypress
|
||||
|
||||
- name: Set up environment variables
|
||||
run: |
|
||||
echo "TOOLJET_HOST=http://localhost:3000" >> .env
|
||||
echo "LOCKBOX_MASTER_KEY=cd97331a419c09387bef49787f7da8d2a81d30733f0de6bed23ad8356d2068b2" >> .env
|
||||
echo "SECRET_KEY_BASE=7073b9a35a15dd20914ae17e36a693093f25b74b96517a5fec461fc901c51e011cd142c731bee48c5081ec8bac321c1f259ef097ef2a16f25df17a3798c03426" >> .env
|
||||
echo "PG_DB=tooljet_development" >> .env
|
||||
echo "PG_USER=postgres" >> .env
|
||||
echo "PG_HOST=postgres" >> .env
|
||||
echo "PG_PASS=postgres" >> .env
|
||||
echo "PG_PORT=5432" >> .env
|
||||
echo "ENABLE_TOOLJET_DB=true" >> .env
|
||||
echo "TOOLJET_DB=tooljet_db" >> .env
|
||||
echo "TOOLJET_DB_USER=postgres" >> .env
|
||||
echo "TOOLJET_DB_HOST=postgres" >> .env
|
||||
echo "TOOLJET_DB_PASS=postgres" >> .env
|
||||
echo "PGRST_JWT_SECRET=r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj" >> .env
|
||||
echo "PGRST_HOST=postgrest" >> .env
|
||||
echo "PGRST_DB_URI=postgres://postgres:postgres@postgres/tooljet_db" >> .env
|
||||
echo "SSO_GIT_OAUTH2_CLIENT_ID=dummy" >> .env
|
||||
echo "SSO_GIT_OAUTH2_CLIENT_SECRET=dummy" >> .env
|
||||
echo "SSO_GIT_OAUTH2_HOST=dummy" >> .env
|
||||
echo "SSO_GOOGLE_OAUTH2_CLIENT_ID=dummy" >> .env
|
||||
echo "SUB_PATH=/apps/tooljet/" >> .env
|
||||
echo "NODE_ENV=production" >> .env
|
||||
echo "SERVE_CLIENT=true" >> .env
|
||||
|
||||
- name: Pulling the docker-compose file
|
||||
run: curl -LO https://tooljet-test.s3.us-west-1.amazonaws.com/docker-compose.yaml && mkdir postgres_data
|
||||
|
||||
- name: Run docker-compose file
|
||||
run: docker-compose up -d
|
||||
|
||||
- name: Checking containers
|
||||
run: docker ps -a
|
||||
|
||||
- name: docker logs
|
||||
run: sudo docker logs Tooljet-app
|
||||
|
||||
- name: Wait for the server to be ready
|
||||
run: |
|
||||
timeout 1500 bash -c '
|
||||
until curl --silent --fail http://localhost:80/apps/tooljet/; do
|
||||
sleep 5
|
||||
done'
|
||||
|
||||
- name: Create Cypress environment file
|
||||
id: create-json
|
||||
uses: jsdaniell/create-json@1.1.2
|
||||
with:
|
||||
name: "cypress.env.json"
|
||||
json: ${{ secrets.CYPRESS_SECRETS }}
|
||||
dir: "./cypress-tests"
|
||||
|
||||
- name: Platform-subpath
|
||||
uses: cypress-io/github-action@v5
|
||||
with:
|
||||
working-directory: ./cypress-tests
|
||||
config: "baseUrl=http://localhost:80/apps/tooljet/"
|
||||
config-file: cypress-workspace.config.js
|
||||
|
||||
- name: Capture Screenshots
|
||||
uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: screenshots
|
||||
path: cypress-tests/cypress/screenshots
|
||||
351
.github/workflows/render-preview-deploy-pro.yml
vendored
|
|
@ -1,351 +0,0 @@
|
|||
name: Render cypress app deploy
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [labeled, unlabeled, closed]
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
create-review-cypress-app:
|
||||
if: ${{ github.event.action == 'labeled' && github.event.label.name == 'create-review-cypress-app' }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Create deployment
|
||||
id: create-deployment
|
||||
run: |
|
||||
export RESPONSE=$(curl --request POST \
|
||||
--url https://api.render.com/v1/services \
|
||||
--header 'accept: application/json' \
|
||||
--header 'content-type: application/json' \
|
||||
--header 'Authorization: Bearer ${{ secrets.RENDER_API_KEY }}' \
|
||||
--data '
|
||||
{
|
||||
"autoDeploy": "yes",
|
||||
"branch": "${{ env.BRANCH_NAME }}",
|
||||
"name": "ToolJet PR CYPRESS #${{ env.PR_NUMBER }}",
|
||||
"notifyOnFail": "default",
|
||||
"ownerId": "tea-caeo4bj19n072h3dddc0",
|
||||
"repo": "${{ github.event.pull_request.head.repo.git_url }}",
|
||||
"slug": "tooljet-pr-cypress-${{ env.PR_NUMBER }}",
|
||||
"suspended": "not_suspended",
|
||||
"suspenders": [],
|
||||
"type": "web_service",
|
||||
"envVars": [
|
||||
{
|
||||
"key": "PG_HOST",
|
||||
"value": "${{ secrets.RENDER_PG_HOST }}"
|
||||
},
|
||||
{
|
||||
"key": "PG_PORT",
|
||||
"value": "5432"
|
||||
},
|
||||
{
|
||||
"key": "PG_USER",
|
||||
"value": "${{ secrets.RENDER_PG_USER }}"
|
||||
},
|
||||
{
|
||||
"key": "PG_PASS",
|
||||
"value": "${{ secrets.RENDER_PG_PASS }}"
|
||||
},
|
||||
{
|
||||
"key": "PG_DB",
|
||||
"value": "${{ env.PR_NUMBER }}_cypress"
|
||||
},
|
||||
{
|
||||
"key": "ENABLE_TOOLJET_DB",
|
||||
"value": "true"
|
||||
},
|
||||
{
|
||||
"key": "TOOLJET_DB",
|
||||
"value": "${{ env.PR_NUMBER }}_cypress"
|
||||
},
|
||||
{
|
||||
"key": "TOOLJET_DB_HOST",
|
||||
"value": "${{ secrets.RENDER_PG_HOST }}"
|
||||
},
|
||||
{
|
||||
"key": "TOOLJET_DB_USER",
|
||||
"value": "${{ secrets.RENDER_PG_USER }}"
|
||||
},
|
||||
{
|
||||
"key": "TOOLJET_DB_PASS",
|
||||
"value": "${{ secrets.RENDER_PG_PASS }}"
|
||||
},
|
||||
{
|
||||
"key": "TOOLJET_DB_PORT",
|
||||
"value": "5432"
|
||||
},
|
||||
{
|
||||
"key": "PGRST_DB_URI",
|
||||
"value": "postgres://${{ secrets.RENDER_PG_USER }}:${{ secrets.RENDER_PG_PASS }}@${{ secrets.RENDER_PG_HOST }}/${{ env.PR_NUMBER }}_cypress"
|
||||
},
|
||||
{
|
||||
"key": "PGRST_HOST",
|
||||
"value": "127.0.0.1:3000"
|
||||
},
|
||||
{
|
||||
"key": "PGRST_JWT_SECRET",
|
||||
"value": "r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj"
|
||||
},
|
||||
{
|
||||
"key": "PGRST_LOG_LEVEL",
|
||||
"value": "info"
|
||||
},
|
||||
{
|
||||
"key": "PORT",
|
||||
"value": "80"
|
||||
},
|
||||
{
|
||||
"key": "TOOLJET_HOST",
|
||||
"value": "https://tooljet-pr-cypress-${{ env.PR_NUMBER }}.onrender.com"
|
||||
},
|
||||
{
|
||||
"key": "DISABLE_TOOLJET_TELEMETRY",
|
||||
"value": "true"
|
||||
},
|
||||
{
|
||||
"key": "SMTP_ADDRESS",
|
||||
"value": "smtp.mailtrap.io"
|
||||
},
|
||||
{
|
||||
"key": "SMTP_DOMAIN",
|
||||
"value": "smtp.mailtrap.io"
|
||||
},
|
||||
{
|
||||
"key": "SMTP_PORT",
|
||||
"value": "2525"
|
||||
},
|
||||
{
|
||||
"key": "SMTP_USERNAME",
|
||||
"value": "${{ secrets.RENDER_SMTP_USERNAME }}"
|
||||
},
|
||||
{
|
||||
"key": "SMTP_PASSWORD",
|
||||
"value": "${{ secrets.RENDER_SMTP_PASSWORD }}"
|
||||
},
|
||||
{
|
||||
"key": "ENABLE_MARKETPLACE_FEATURE",
|
||||
"value": "true"
|
||||
},
|
||||
{
|
||||
"key": "SSO_GIT_OAUTH2_CLIENT_ID",
|
||||
"value": "dummy"
|
||||
},
|
||||
{
|
||||
"key": "SSO_GIT_OAUTH2_CLIENT_SECRET",
|
||||
"value": "dummy"
|
||||
},
|
||||
{
|
||||
"key": "SSO_GIT_OAUTH2_HOST",
|
||||
"value": "dummy"
|
||||
},
|
||||
{
|
||||
"key": "SSO_GOOGLE_OAUTH2_CLIENT_ID",
|
||||
"value": "dummy"
|
||||
}
|
||||
],
|
||||
"serviceDetails": {
|
||||
"disk": null,
|
||||
"env": "docker",
|
||||
"envSpecificDetails": {
|
||||
"dockerCommand": "",
|
||||
"dockerContext": "./",
|
||||
"dockerfilePath": "./docker/preview.Dockerfile"
|
||||
},
|
||||
"healthCheckPath": "/api/health",
|
||||
"numInstances": 1,
|
||||
"openPorts": [{
|
||||
"port": 80,
|
||||
"protocol": "TCP"
|
||||
}],
|
||||
"plan": "pro",
|
||||
"pullRequestPreviewsEnabled": "no",
|
||||
"region": "oregon",
|
||||
"url": "https://tooljet-pr-cypress-${{ env.PR_NUMBER }}.onrender.com"
|
||||
}
|
||||
}')
|
||||
|
||||
echo "response: $RESPONSE"
|
||||
export SERVICE_ID=$(echo $RESPONSE | jq -r '.service.id')
|
||||
echo "SERVICE_ID=$SERVICE_ID" >> $GITHUB_ENV
|
||||
|
||||
- name: Comment deployment URL
|
||||
uses: actions/github-script@v5
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: 'Deployment: https://tooljet-pr-cypress-${{ env.PR_NUMBER }}.onrender.com \n Dashboard: https://dashboard.render.com/web/${{ env.SERVICE_ID }}'
|
||||
})
|
||||
|
||||
- uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: 'create-review-cypress-app'
|
||||
})
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
|
||||
await github.rest.issues.addLabels({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: ['active-review-cypress-app']
|
||||
})
|
||||
|
||||
destroy-review-cypress-app:
|
||||
if: ${{ (github.event.action == 'labeled' && github.event.label.name == 'destroy-review-cypress-app') || github.event.action == 'closed' }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Delete service
|
||||
run: |
|
||||
export SERVICE_ID=$(curl --request GET \
|
||||
--url 'https://api.render.com/v1/services?name=ToolJet%20PR%20CYPRESS%20%23${{ env.PR_NUMBER }}&limit=1' \
|
||||
--header 'accept: application/json' \
|
||||
--header 'authorization: Bearer ${{ secrets.RENDER_API_KEY }}' | \
|
||||
jq -r '.[0].service.id')
|
||||
|
||||
curl --request DELETE \
|
||||
--url https://api.render.com/v1/services/$SERVICE_ID \
|
||||
--header 'accept: application/json' \
|
||||
--header 'authorization: Bearer ${{ secrets.RENDER_API_KEY }}'
|
||||
|
||||
- uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: 'destroy-review-cypress-app'
|
||||
})
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: 'suspend-review-cypress-app'
|
||||
})
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: 'active-review-cypress-app'
|
||||
})
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
|
||||
- name: Install PostgreSQL client
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install postgresql-client -y
|
||||
|
||||
- name: Wait after installing PostgreSQL
|
||||
run: sleep 25
|
||||
|
||||
- name: Drop PostgreSQL PR database
|
||||
env:
|
||||
PGHOST: ${{ secrets.RENDER_DS_PG_HOST }}
|
||||
PGPORT: 5432
|
||||
PGUSER: ${{ secrets.RENDER_DS_PG_USER }}
|
||||
PGDATABASE: ${{ env.PR_NUMBER }}_cypress
|
||||
run: |
|
||||
PGPASSWORD=${{ secrets.RENDER_DS_PG_PASS }} psql -h $PGHOST -p $PGPORT -U $PGUSER -d postgres -c "drop database \"$PGDATABASE\" ;"
|
||||
|
||||
suspend-review-cypress-app:
|
||||
if: ${{ github.event.action == 'labeled' && github.event.label.name == 'suspend-review-cypress-app' }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Suspend service
|
||||
run: |
|
||||
export SERVICE_ID=$(curl --request GET \
|
||||
--url 'https://api.render.com/v1/services?name=ToolJet%20PR%20CYPRESS%20%23${{ env.PR_NUMBER }}&limit=1' \
|
||||
--header 'accept: application/json' \
|
||||
--header 'authorization: Bearer ${{ secrets.RENDER_API_KEY }}' | \
|
||||
jq -r '.[0].service.id')
|
||||
|
||||
curl --request POST \
|
||||
--url https://api.render.com/v1/services/$SERVICE_ID/suspend \
|
||||
--header 'accept: application/json' \
|
||||
--header 'authorization: Bearer ${{ secrets.RENDER_API_KEY }}'
|
||||
|
||||
- uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: 'active-review-cypress-app'
|
||||
})
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
|
||||
resume-review-cypress-app:
|
||||
if: ${{ github.event.action == 'unlabeled' && github.event.label.name == 'suspend-review-cypress-app' }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Resume service
|
||||
run: |
|
||||
export SERVICE_ID=$(curl --request GET \
|
||||
--url 'https://api.render.com/v1/services?name=ToolJet%20PR%20CYPRESS%20%23${{ env.PR_NUMBER }}&limit=1' \
|
||||
--header 'accept: application/json' \
|
||||
--header 'authorization: Bearer ${{ secrets.RENDER_API_KEY }}' | \
|
||||
jq -r '.[0].service.id')
|
||||
|
||||
curl --request POST \
|
||||
--url https://api.render.com/v1/services/$SERVICE_ID/resume \
|
||||
--header 'accept: application/json' \
|
||||
--header 'authorization: Bearer ${{ secrets.RENDER_API_KEY }}'
|
||||
|
||||
- uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.addLabels({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: ['active-review-cypress-app']
|
||||
})
|
||||
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: 'suspend-review-cypress-app'
|
||||
})
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
45
.github/workflows/render-suspend-labeler-pro.yml
vendored
|
|
@ -1,45 +0,0 @@
|
|||
name: Label for stale render cypress app deploy
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
label-stale-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-review-cypress-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-review-cypress-app']
|
||||
})
|
||||
}
|
||||
2
.version
|
|
@ -1 +1 @@
|
|||
2.19.2
|
||||
2.20.1
|
||||
|
|
|
|||
79
docs/docs/user-authentication/sso/saml.md
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
---
|
||||
id: saml
|
||||
title: SAML
|
||||
---
|
||||
|
||||
ToolJet supports SAML authentication for your workspace. The supported SAML providers are: Okta, Active Directory Federation Services, Azure AD, Auth0 and other SAML SSO providers.
|
||||
|
||||
### Configuring SAML
|
||||
|
||||
To enable SAML authentication, you need to configure the following workspace settings:
|
||||
|
||||
1. Go to **Workspace Settings** > **SSO** > **SAML**.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||
<img className="screenshot-full" src="/img/sso/saml/workspaceset.png" alt="SSO :SAMP" />
|
||||
|
||||
</div>
|
||||
|
||||
2. By default, SAML is disabled. Toggle it on to enable SAML authentication.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||
<img className="screenshot-full" src="/img/sso/saml/enable.png" alt="SSO :SAMP" />
|
||||
|
||||
</div>
|
||||
|
||||
3. Enter the following configuration details:
|
||||
|
||||
- **SAML Provider Name**: Enter the name of your SAML provider. This name will be displayed on the login page.
|
||||
- **Identity provider metadata**: Upload the data from the metadata file provided by your SAML provider. This file contains the SAML configuration details.
|
||||
- **Group Attribute**: Enter the name of the attribute that contains the group information of the user. This attribute is used to map the user to the appropriate group.
|
||||
- **Redirect URL**: Copy the redirect URL provided and paste it in the SAML provider's configuration page.
|
||||
|
||||
:::tip Downloading the metadata from your identity provider
|
||||
Generally, the metadata is available in the form of an XML file which can be downloaded from your identity provider's dashboard.
|
||||
|
||||
Copy the metadata from the XML file and paste it into the ToolJet's SAML SSO configuration settings. Please ensure that the metadata is pasted in the correct format, as it contains essential configuration details from the identity provider necessary for authentication.
|
||||
|
||||
Additionally, you can often find this data by navigating to https://<your-identity-provider>/federationmetadata/2007-06/federationmetadata.xml
|
||||
:::
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||
<img className="screenshot-full" src="/img/sso/saml/config.png" alt="SSO :SAMP" />
|
||||
|
||||
</div>
|
||||
|
||||
4. Once configured, click **Save Changes**.
|
||||
|
||||
### Logging in with SAML
|
||||
|
||||
1. Go to the **[General Settings](/docs/user-authentication/general-settings)** and copy the **Login URL** provided. Furthermore, you have the flexibility to choose whether to turn on 'Enable Signups,' allowing users to signup without an invite. Through SSO authentication, we check if the user already exists; if so, they can sign in seamlessly. Otherwise, an error will be displayed. Conversely, with this option disabled, only invited users can log in, provided SSO authentication is successful.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||
<img className="screenshot-full" src="/img/sso/saml/url.png" alt="SSO :SAML"/>
|
||||
|
||||
</div>
|
||||
|
||||
2. The **Login URL** obtained can be used to access the workspace. Please note that ToolJet supports SAML login at the workspace level, ensuring users are logged in specifically to the selected workspace.
|
||||
|
||||
As a result, users can now log in to your workspace using the provided Login URL. The login page will prominently feature the name of the SAML provider configured in your workspace settings.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||
<img className="screenshot-full" src="/img/sso/saml/login.png" alt="SSO :SAMP" />
|
||||
|
||||
</div>
|
||||
|
||||
3. Click on **Sign in with `SAML Name`** button and you will be redirected to the SAML provider's login page.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||
<img className="screenshot-full" src="/img/sso/saml/auth.png" alt="SSO :SAMP" />
|
||||
|
||||
</div>
|
||||
|
||||
4. Enter your credentials and click **Login**. If the user is signing in for the first time, they will be redirected to the ToolJet's onboarding page.
|
||||
|
|
@ -271,6 +271,7 @@ const sidebars = {
|
|||
],
|
||||
},
|
||||
'user-authentication/sso/ldap',
|
||||
'user-authentication/sso/saml',
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
|||
BIN
docs/static/img/sso/saml/auth.png
vendored
Normal file
|
After Width: | Height: | Size: 192 KiB |
BIN
docs/static/img/sso/saml/config.png
vendored
Normal file
|
After Width: | Height: | Size: 431 KiB |
BIN
docs/static/img/sso/saml/enable.png
vendored
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
docs/static/img/sso/saml/login.png
vendored
Normal file
|
After Width: | Height: | Size: 174 KiB |
BIN
docs/static/img/sso/saml/url.png
vendored
Normal file
|
After Width: | Height: | Size: 367 KiB |
BIN
docs/static/img/sso/saml/workspaceset.png
vendored
Normal file
|
After Width: | Height: | Size: 102 KiB |
|
|
@ -0,0 +1,79 @@
|
|||
---
|
||||
id: saml
|
||||
title: SAML
|
||||
---
|
||||
|
||||
ToolJet supports SAML authentication for your workspace. The supported SAML providers are: Okta, Active Directory Federation Services, Azure AD, Auth0 and other SAML SSO providers.
|
||||
|
||||
### Configuring SAML
|
||||
|
||||
To enable SAML authentication, you need to configure the following workspace settings:
|
||||
|
||||
1. Go to **Workspace Settings** > **SSO** > **SAML**.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||
<img className="screenshot-full" src="/img/sso/saml/workspaceset.png" alt="SSO :SAMP" />
|
||||
|
||||
</div>
|
||||
|
||||
2. By default, SAML is disabled. Toggle it on to enable SAML authentication.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||
<img className="screenshot-full" src="/img/sso/saml/enable.png" alt="SSO :SAMP" />
|
||||
|
||||
</div>
|
||||
|
||||
3. Enter the following configuration details:
|
||||
|
||||
- **SAML Provider Name**: Enter the name of your SAML provider. This name will be displayed on the login page.
|
||||
- **Identity provider metadata**: Upload the data from the metadata file provided by your SAML provider. This file contains the SAML configuration details.
|
||||
- **Group Attribute**: Enter the name of the attribute that contains the group information of the user. This attribute is used to map the user to the appropriate group.
|
||||
- **Redirect URL**: Copy the redirect URL provided and paste it in the SAML provider's configuration page.
|
||||
|
||||
:::tip Downloading the metadata from your identity provider
|
||||
Generally, the metadata is available in the form of an XML file which can be downloaded from your identity provider's dashboard.
|
||||
|
||||
Copy the metadata from the XML file and paste it into the ToolJet's SAML SSO configuration settings. Please ensure that the metadata is pasted in the correct format, as it contains essential configuration details from the identity provider necessary for authentication.
|
||||
|
||||
Additionally, you can often find this data by navigating to https://<your-identity-provider>/federationmetadata/2007-06/federationmetadata.xml
|
||||
:::
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||
<img className="screenshot-full" src="/img/sso/saml/config.png" alt="SSO :SAMP" />
|
||||
|
||||
</div>
|
||||
|
||||
4. Once configured, click **Save Changes**.
|
||||
|
||||
### Logging in with SAML
|
||||
|
||||
1. Go to the **[General Settings](/docs/user-authentication/general-settings)** and copy the **Login URL** provided. Furthermore, you have the flexibility to choose whether to turn on 'Enable Signups,' allowing users to signup without an invite. Through SSO authentication, we check if the user already exists; if so, they can sign in seamlessly. Otherwise, an error will be displayed. Conversely, with this option disabled, only invited users can log in, provided SSO authentication is successful.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||
<img className="screenshot-full" src="/img/sso/saml/url.png" alt="SSO :SAML"/>
|
||||
|
||||
</div>
|
||||
|
||||
2. The **Login URL** obtained can be used to access the workspace. Please note that ToolJet supports SAML login at the workspace level, ensuring users are logged in specifically to the selected workspace.
|
||||
|
||||
As a result, users can now log in to your workspace using the provided Login URL. The login page will prominently feature the name of the SAML provider configured in your workspace settings.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||
<img className="screenshot-full" src="/img/sso/saml/login.png" alt="SSO :SAMP" />
|
||||
|
||||
</div>
|
||||
|
||||
3. Click on **Sign in with `SAML Name`** button and you will be redirected to the SAML provider's login page.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
|
||||
<img className="screenshot-full" src="/img/sso/saml/auth.png" alt="SSO :SAMP" />
|
||||
|
||||
</div>
|
||||
|
||||
4. Enter your credentials and click **Login**. If the user is signing in for the first time, they will be redirected to the ToolJet's onboarding page.
|
||||
|
|
@ -260,7 +260,8 @@
|
|||
"user-authentication/sso/openid/google-openid"
|
||||
]
|
||||
},
|
||||
"user-authentication/sso/ldap"
|
||||
"user-authentication/sso/ldap",
|
||||
"user-authentication/sso/saml"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -421,4 +422,4 @@
|
|||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
2.19.2
|
||||
2.20.1
|
||||
|
|
|
|||
BIN
frontend/assets/images/templates/customer-support-admin-dark.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
frontend/assets/images/templates/customer-support-admin.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 53 KiB |
BIN
frontend/assets/images/templates/customer-ticketing-form.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
frontend/assets/images/templates/inventory-management-dark.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
frontend/assets/images/templates/inventory-management.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
frontend/assets/images/templates/lead-management-system-dark.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
frontend/assets/images/templates/lead-management-system.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 64 KiB |
BIN
frontend/assets/images/templates/sales-analytics-dashboard.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 239 KiB |
BIN
frontend/assets/images/templates/supply-chain-management.png
Normal file
|
After Width: | Height: | Size: 238 KiB |
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
|
||||
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
||||
|
||||
export const Pagination = function Pagination({
|
||||
onPageIndexChanged,
|
||||
|
|
@ -16,6 +17,7 @@ export const Pagination = function Pagination({
|
|||
// eslint-disable-next-line no-unused-vars
|
||||
darkMode,
|
||||
tableWidth,
|
||||
loadingState,
|
||||
}) {
|
||||
const [pageCount, setPageCount] = useState(autoPageCount);
|
||||
|
||||
|
|
@ -50,6 +52,16 @@ export const Pagination = function Pagination({
|
|||
gotoPage(pageIndex - 1);
|
||||
}
|
||||
|
||||
if (loadingState) {
|
||||
return (
|
||||
<div className="w-100">
|
||||
<SkeletonTheme baseColor="var(--slate3)" width="100%">
|
||||
<Skeleton count={1} width={'100%'} height={28} className="mb-1" />
|
||||
</SkeletonTheme>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pagination-container d-flex h-100 align-items-center custom-gap-4" data-cy="pagination-section">
|
||||
<div className="d-flex">
|
||||
|
|
|
|||
|
|
@ -1547,15 +1547,7 @@ export function Table({
|
|||
))}
|
||||
</div>
|
||||
<div className={`col d-flex justify-content-center h-100 ${loadingState && 'w-100'}`}>
|
||||
{loadingState && (
|
||||
<div className="w-100">
|
||||
<SkeletonTheme baseColor="var(--slate3)" width="100%">
|
||||
<Skeleton count={1} width={'100%'} height={28} className="mb-1" />
|
||||
</SkeletonTheme>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{enablePagination && !loadingState && (
|
||||
{enablePagination && (
|
||||
<Pagination
|
||||
lastActivePageIndex={pageIndex}
|
||||
serverSide={serverSidePagination}
|
||||
|
|
@ -1570,6 +1562,7 @@ export function Table({
|
|||
enablePrevButton={enablePrevButton}
|
||||
darkMode={darkMode}
|
||||
tableWidth={width}
|
||||
loadingState={loadingState}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -689,7 +689,7 @@ export const Container = ({
|
|||
our
|
||||
<a
|
||||
className="color-indigo9 "
|
||||
href="https://docs.tooljet.com/docs#the-very-quick-quickstart"
|
||||
href="https://docs.tooljet.com/docs/#quickstart-guide"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ export default function TemplateLibraryModal(props) {
|
|||
toast.success('App created.', {
|
||||
position: 'top-center',
|
||||
});
|
||||
navigate(`/${getWorkspaceId()}/apps/${data.id}`);
|
||||
navigate(`/${getWorkspaceId()}/apps/${data.app[0].id}`);
|
||||
})
|
||||
.catch((e) => {
|
||||
toast.error(e.error, {
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
2.19.2
|
||||
2.20.1
|
||||
|
|
|
|||
|
|
@ -21,9 +21,8 @@ export class LibraryAppsController {
|
|||
if (!ability.can('createApp', App)) {
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
}
|
||||
const newApp = await this.libraryAppCreationService.perform(user, identifier);
|
||||
|
||||
return newApp;
|
||||
const result = await this.libraryAppCreationService.perform(user, identifier);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Get()
|
||||
|
|
|
|||
|
|
@ -48,5 +48,6 @@ if (process.env.ENABLE_TOOLJET_DB === 'true') {
|
|||
CredentialsService,
|
||||
PostgrestProxyService,
|
||||
],
|
||||
exports: [ImportExportResourcesService],
|
||||
})
|
||||
export class ImportExportResourcesModule {}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,14 @@ import { PluginsService } from '@services/plugins.service';
|
|||
import { Plugin } from 'src/entities/plugin.entity';
|
||||
import { PluginsHelper } from 'src/helpers/plugins.helper';
|
||||
import { AppEnvironmentService } from '@services/app_environments.service';
|
||||
import { ImportExportResourcesModule } from '../import_export_resources/import_export_resources.module';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([App, Credential, File, Plugin, DataSource]), CaslModule],
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([App, Credential, File, Plugin, DataSource]),
|
||||
CaslModule,
|
||||
ImportExportResourcesModule,
|
||||
],
|
||||
providers: [
|
||||
EncryptionService,
|
||||
CredentialsService,
|
||||
|
|
|
|||
|
|
@ -1,30 +1,63 @@
|
|||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { App } from '../entities/app.entity';
|
||||
import { User } from '../entities/user.entity';
|
||||
import { AppImportExportService } from './app_import_export.service';
|
||||
import { readFileSync } from 'fs';
|
||||
import { Logger } from 'nestjs-pino';
|
||||
import { ImportExportResourcesService } from './import_export_resources.service';
|
||||
import { ImportResourcesDto } from '@dto/import-resources.dto';
|
||||
import { AppImportExportService } from './app_import_export.service';
|
||||
|
||||
@Injectable()
|
||||
export class LibraryAppCreationService {
|
||||
constructor(private readonly appImportExportService: AppImportExportService, private readonly logger: Logger) {}
|
||||
constructor(
|
||||
private readonly importExportResourcesService: ImportExportResourcesService,
|
||||
private readonly appImportExportService: AppImportExportService,
|
||||
private readonly logger: Logger
|
||||
) {}
|
||||
|
||||
async perform(currentUser: User, identifier: string): Promise<App> {
|
||||
const newApp = await this.appImportExportService.import(currentUser, this.findAppDefinition(identifier));
|
||||
async perform(currentUser: User, identifier: string) {
|
||||
const templateDefinition = this.findTemplateDefinition(identifier);
|
||||
const importDto = new ImportResourcesDto();
|
||||
importDto.organization_id = currentUser.organizationId;
|
||||
importDto.app = templateDefinition.app || templateDefinition.appV2;
|
||||
importDto.tooljet_database = templateDefinition.tooljet_database;
|
||||
|
||||
return newApp;
|
||||
if (this.isVersionGreaterThanOrEqual(templateDefinition.tooljet_version, '2.16.0')) {
|
||||
return await this.importExportResourcesService.import(currentUser, importDto);
|
||||
} else {
|
||||
const importedApp = await this.appImportExportService.import(currentUser, templateDefinition);
|
||||
return {
|
||||
app: [importedApp],
|
||||
tooljet_database: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
findAppDefinition(identifier: string) {
|
||||
let appDefinition: object;
|
||||
|
||||
findTemplateDefinition(identifier: string) {
|
||||
try {
|
||||
appDefinition = JSON.parse(readFileSync(`templates/${identifier}/definition.json`, 'utf-8'));
|
||||
return JSON.parse(readFileSync(`templates/${identifier}/definition.json`, 'utf-8'));
|
||||
} catch (err) {
|
||||
this.logger.error(err);
|
||||
throw new BadRequestException('App definition not found');
|
||||
}
|
||||
}
|
||||
|
||||
return appDefinition;
|
||||
isVersionGreaterThanOrEqual(version1: string, version2: string) {
|
||||
if (!version1) return false;
|
||||
|
||||
const v1Parts = version1.split('-')[0].split('.').map(Number);
|
||||
const v2Parts = version2.split('-')[0].split('.').map(Number);
|
||||
|
||||
for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
|
||||
const v1Part = +v1Parts[i] || 0;
|
||||
const v2Part = +v2Parts[i] || 0;
|
||||
|
||||
if (v1Part < v2Part) {
|
||||
return false;
|
||||
} else if (v1Part > v2Part) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
60908
server/templates/customer-support-admin/definition.json
Normal file
13
server/templates/customer-support-admin/manifest.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "Customer support admin",
|
||||
"description": "The Customer Support Admin template streamlines support with a Main Dashboard for ticket management and All Contacts for maintaining customer data.",
|
||||
"widgets": ["Table", "Chart"],
|
||||
"sources": [
|
||||
{
|
||||
"name": "Tooljet Database",
|
||||
"id": "tooljetdb"
|
||||
}
|
||||
],
|
||||
"id": "customer-support-admin",
|
||||
"category": "operations"
|
||||
}
|
||||
29622
server/templates/customer-ticketing-form/definition.json
Normal file
13
server/templates/customer-ticketing-form/manifest.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "Customer ticketing form",
|
||||
"description": "The Customer Ticketing Form optimizes support ticket management, seamlessly gathering customer info and tracking ticket progress with deep integration into Customer Support Admin.",
|
||||
"widgets": ["Table", "Chart"],
|
||||
"sources": [
|
||||
{
|
||||
"name": "Tooljet Database",
|
||||
"id": "tooljetdb"
|
||||
}
|
||||
],
|
||||
"id": "customer-ticketing-form",
|
||||
"category": "operations"
|
||||
}
|
||||
39620
server/templates/inventory-management/definition.json
Normal file
13
server/templates/inventory-management/manifest.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "Inventory management",
|
||||
"description": "Easily manage, control, and optimise your inventory with our single-page Inventory Management Template.",
|
||||
"widgets": ["Table", "Chart"],
|
||||
"sources": [
|
||||
{
|
||||
"name": "Tooljet Database",
|
||||
"id": "tooljetdb"
|
||||
}
|
||||
],
|
||||
"id": "inventory-management",
|
||||
"category": "operations"
|
||||
}
|
||||
43519
server/templates/lead-management-system/definition.json
Normal file
13
server/templates/lead-management-system/manifest.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "Lead management system",
|
||||
"description": "The Lead Management System template streamlines lead lifecycle with four status templates: Leads (capturing potential leads) and Opportunities (converting leads), along with Customers (effective relationship management) and Lost (learning from lost opportunities).",
|
||||
"widgets": ["Table", "Chart"],
|
||||
"sources": [
|
||||
{
|
||||
"name": "Tooljet Database",
|
||||
"id": "tooljetdb"
|
||||
}
|
||||
],
|
||||
"id": "lead-management-system",
|
||||
"category": "sales"
|
||||
}
|
||||
70760
server/templates/sales-analytics-dashboard/definition.json
Normal file
13
server/templates/sales-analytics-dashboard/manifest.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "Sales analytics dashboard",
|
||||
"description": "The Sales Analytics Dashboard template offers comprehensive sales monitoring and insights with four key sections: Main Dashboard (critical metrics), Orders (order details), Customers (demographics & history), and Products (product performance).",
|
||||
"widgets": ["Table", "Chart"],
|
||||
"sources": [
|
||||
{
|
||||
"name": "Tooljet Database",
|
||||
"id": "tooljetdb"
|
||||
}
|
||||
],
|
||||
"id": "sales-analytics-dashboard",
|
||||
"category": "sales"
|
||||
}
|
||||
111567
server/templates/supply-chain-management/definition.json
Normal file
13
server/templates/supply-chain-management/manifest.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "Supply chain management",
|
||||
"description": "The Supply Chain Management template optimizes operations with a Main Dashboard for KPIs, Product Inventory for stock management, and All Orders for tracking and updates.",
|
||||
"widgets": ["Table", "Chart"],
|
||||
"sources": [
|
||||
{
|
||||
"name": "Tooljet Database",
|
||||
"id": "tooljetdb"
|
||||
}
|
||||
],
|
||||
"id": "supply-chain-management",
|
||||
"category": "operations"
|
||||
}
|
||||
|
|
@ -42,12 +42,12 @@ describe('library apps controller', () => {
|
|||
|
||||
response = await request(app.getHttpServer())
|
||||
.post('/api/library_apps')
|
||||
.send({ identifier: 'github-contributors' })
|
||||
.send({ identifier: 'supply-chain-management' })
|
||||
.set('tj-workspace-id', adminUserData.user.defaultOrganizationId)
|
||||
.set('Cookie', adminUserData['tokenCookie']);
|
||||
|
||||
expect(response.statusCode).toBe(201);
|
||||
expect(response.body.name).toContain('GitHub Contributor Leaderboard');
|
||||
expect(response.body.app[0].name).toContain('Supply Chain Management');
|
||||
});
|
||||
|
||||
it('should return error if template identifier is not found', async () => {
|
||||
|
|
|
|||