mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-22 16:38:21 +00:00
Merge branch 'appbuilder/sprint-13' into gh-11817-listview-margin
This commit is contained in:
commit
6b6375d85c
118 changed files with 8210 additions and 731 deletions
197
.github/workflows/cypress-platform.yml
vendored
197
.github/workflows/cypress-platform.yml
vendored
|
|
@ -12,91 +12,106 @@ env:
|
|||
jobs:
|
||||
Cypress-Platform:
|
||||
runs-on: ubuntu-22.04
|
||||
if: contains(github.event.pull_request.labels.*.name, 'run-cypress') ||
|
||||
contains(github.event.pull_request.labels.*.name, 'run-cypress-platform-ce') ||
|
||||
contains(github.event.pull_request.labels.*.name, 'run-cypress-platform-ee') ||
|
||||
contains(github.event.pull_request.labels.*.name, 'run-cypress-ce')
|
||||
if: contains(github.event.pull_request.labels.*.name, 'run-cypress') ||
|
||||
contains(github.event.pull_request.labels.*.name, 'run-cypress-platform-ce') ||
|
||||
contains(github.event.pull_request.labels.*.name, 'run-cypress-platform-ee') ||
|
||||
contains(github.event.pull_request.labels.*.name, 'run-cypress-ce')
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
edition: >-
|
||||
${{
|
||||
contains(github.event.pull_request.labels.*.name, 'run-cypress') && fromJson('["ce", "ee"]') ||
|
||||
contains(github.event.pull_request.labels.*.name, 'run-cypress-ce') && fromJson('["ce"]') ||
|
||||
contains(github.event.pull_request.labels.*.name, 'run-cypress-platform-ce') && fromJson('["ce"]') ||
|
||||
contains(github.event.pull_request.labels.*.name, 'run-cypress-platform-ee') && fromJson('["ee"]') ||
|
||||
fromJson('[]')
|
||||
}}
|
||||
edition:
|
||||
- ${{ contains(github.event.pull_request.labels.*.name, 'run-cypress') && 'ce' || contains(github.event.pull_request.labels.*.name, 'run-cypress-platform-ce') && 'ce' || contains(github.event.pull_request.labels.*.name, 'run-cypress-ce') && 'ce' || '' }}
|
||||
- ${{ contains(github.event.pull_request.labels.*.name, 'run-cypress') && 'ee' || contains(github.event.pull_request.labels.*.name, 'run-cypress-platform-ee') && 'ee' || '' }}
|
||||
exclude:
|
||||
- edition: ""
|
||||
|
||||
steps:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18.18.2
|
||||
|
||||
- name: Set up Git authentication for private submodules
|
||||
- name: Debug labels and matrix edition
|
||||
run: |
|
||||
git config --global url."https://x-access-token:${{ secrets.CUSTOM_GITHUB_TOKEN }}@github.com/".insteadOf "https://github.com/"
|
||||
echo "Labels: ${{ toJSON(github.event.pull_request.labels.*.name) }}"
|
||||
echo "Matrix edition: ${{ matrix.edition }}"
|
||||
|
||||
- name: Checkout with Submodules
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
- name: Checking out the correct branch for submodules EE
|
||||
# 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
|
||||
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@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Set SAFE_BRANCH_NAME
|
||||
run: echo "SAFE_BRANCH_NAME=$(echo ${{ env.BRANCH_NAME }} | tr '/' '-')" >> $GITHUB_ENV
|
||||
|
||||
- name: Build CE Docker image
|
||||
if: matrix.edition == 'ce'
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: docker/ce-production.Dockerfile
|
||||
push: true
|
||||
tags: tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ce
|
||||
platforms: linux/amd64
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build EE Docker image
|
||||
if: matrix.edition == 'ee'
|
||||
run: |
|
||||
git submodule update --init --recursive
|
||||
git submodule foreach --recursive '
|
||||
git checkout ${{ env.BRANCH_NAME }} 2>/dev/null || git checkout main'
|
||||
|
||||
- name: Set up Docker
|
||||
uses: docker-practice/actions-setup-docker@master
|
||||
|
||||
- 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: Local development setup
|
||||
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: Run PostgREST Docker Container
|
||||
run: |
|
||||
sudo docker run -d --name postgrest --network tooljet -p 3001:3000 \
|
||||
-e PGRST_DB_URI="postgres://postgres:postgres@localhost:5432/tooljet" \
|
||||
-e PGRST_DB_ANON_ROLE="postgres" \
|
||||
-e PGRST_JWT_SECRET="r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj" \
|
||||
-e PGRST_DB_PRE_CONFIG=postgrest.pre_config \
|
||||
postgrest/postgrest:v12.2.0
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
build-args: |
|
||||
CUSTOM_GITHUB_TOKEN=${{ secrets.CUSTOM_GITHUB_TOKEN }}
|
||||
BRANCH_NAME=${{ github.event.pull_request.head.ref }}
|
||||
file: cypress-tests/cypress.Dockerfile
|
||||
push: true
|
||||
tags: tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ee
|
||||
platforms: linux/amd64
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Set up environment variables
|
||||
run: |
|
||||
echo "TOOLJET_EDITION=${{ matrix.edition == 'ee' && 'ee' || 'ce' }}" >> .env
|
||||
echo "TOOLJET_HOST=http://localhost:8082" >> .env
|
||||
echo "TOOLJET_EDITION=${{ matrix.edition }}" >> .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_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 "TOOLJET_DB_STATEMENT_TIMEOUT=60000" >> .env
|
||||
echo "TOOLJET_DB_RECONFIG=true" >> .env
|
||||
echo "PGRST_JWT_SECRET=r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj" >> .env
|
||||
echo "PGRST_HOST=localhost:3001" >> .env
|
||||
echo "PGRST_DB_PRE_CONFIG=postgrest.pre_config" >> .env
|
||||
echo "PGRST_DB_URI=postgres://postgres:postgres@localhost:5432/tooljet" >> .env
|
||||
echo "PGRST_DB_URI=postgres://postgres:postgres@postgres/tooljet_db" >> .env
|
||||
echo "ENABLE_MARKETPLACE_FEATURE=true" >> .env
|
||||
echo "ENABLE_MARKETPLACE_DEV_MODE=true" >> .env
|
||||
echo "ENABLE_PRIVATE_APP_EMBED=true" >> .env
|
||||
|
|
@ -105,29 +120,50 @@ jobs:
|
|||
echo "SSO_GIT_OAUTH2_CLIENT_ID=1234567890" >> .env
|
||||
echo "SSO_GIT_OAUTH2_CLIENT_SECRET=3346shfvkdjjsfkvxce32854e026a4531ed" >> .env
|
||||
|
||||
- name: Set up database
|
||||
run: |
|
||||
npm run --prefix server db:create
|
||||
npm run --prefix server db:reset
|
||||
sleep 5
|
||||
# Only add EE-specific env vars if edition is ee
|
||||
if [ "${{ matrix.edition }}" = "ee" ]; then
|
||||
echo "SSO_OPENID_NAME=tj-oidc-simulator" >> .env
|
||||
echo "SSO_OPENID_CLIENT_ID=${{ secrets.SSO_OPENID_CLIENT_ID }}" >> .env
|
||||
echo "SSO_OPENID_CLIENT_SECRET=${{ secrets.SSO_OPENID_CLIENT_SECRET }}" >> .env
|
||||
echo "SSO_OPENID_WELL_KNOWN_URL=http://34.66.166.236:8080/.well-known/openid-configuration" >> .env
|
||||
echo "LICENSE_KEY=${{ secrets.RENDER_LICENSE_KEY }}" >> .env
|
||||
fi
|
||||
|
||||
- name: Start services
|
||||
- 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: Update docker-compose file
|
||||
run: |
|
||||
cd plugins && npm start &
|
||||
cd server && npm run start:dev &
|
||||
cd frontend && npm start &
|
||||
# Update docker-compose.yaml with the appropriate image based on edition
|
||||
if [ "${{ matrix.edition }}" = "ce" ]; then
|
||||
sed -i '/^[[:space:]]*tooljet:/,/^$/ s|^\([[:space:]]*image:[[:space:]]*\).*|\1tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ce|' docker-compose.yaml
|
||||
elif [ "${{ matrix.edition }}" = "ee" ]; then
|
||||
sed -i '/^[[:space:]]*tooljet:/,/^$/ s|^\([[:space:]]*image:[[:space:]]*\).*|\1tooljet/tj-osv:${{ env.SAFE_BRANCH_NAME }}-ee|' docker-compose.yaml
|
||||
fi
|
||||
|
||||
- name: Install Docker Compose
|
||||
run: |
|
||||
curl -L "https://github.com/docker/compose/releases/download/v2.10.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
chmod +x /usr/local/bin/docker-compose
|
||||
|
||||
- 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 300 bash -c '
|
||||
until curl --silent --fail http://localhost:8082; do
|
||||
timeout 500 bash -c '
|
||||
until curl --silent --fail http://localhost:3000; do
|
||||
sleep 5
|
||||
done'
|
||||
|
||||
- name: Postgres logs
|
||||
run: docker logs postgrest
|
||||
|
||||
- name: Create Cypress environment file
|
||||
- name: Create Cypress environment file for CE
|
||||
if: matrix.edition == 'ce'
|
||||
id: create-json
|
||||
uses: jsdaniell/create-json@1.1.2
|
||||
with:
|
||||
|
|
@ -135,13 +171,30 @@ jobs:
|
|||
json: ${{ secrets.CYPRESS_SECRETS }}
|
||||
dir: "./cypress-tests"
|
||||
|
||||
- name: Run Cypress tests
|
||||
- name: Run Cypress tests for CE
|
||||
if: matrix.edition == 'ce'
|
||||
uses: cypress-io/github-action@v6
|
||||
with:
|
||||
working-directory: ./cypress-tests
|
||||
config: "baseUrl=http://localhost:8082"
|
||||
config: "baseUrl=http://localhost:3000"
|
||||
config-file: cypress-platform.config.js
|
||||
|
||||
- name: Create Cypress environment file for EE
|
||||
if: matrix.edition == 'ee'
|
||||
uses: jsdaniell/create-json@1.1.2
|
||||
with:
|
||||
name: "cypress.env.json"
|
||||
json: ${{ secrets.CYPRESS_EE_SECRETS }}
|
||||
dir: "./cypress-tests"
|
||||
|
||||
- name: Run Cypress tests for EE
|
||||
if: matrix.edition == 'ee'
|
||||
uses: cypress-io/github-action@v6
|
||||
with:
|
||||
working-directory: ./cypress-tests
|
||||
config: "baseUrl=http://localhost:3000"
|
||||
config-file: cypress-ee-platform.config.js
|
||||
|
||||
- name: Capture Screenshots
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
|
|
|
|||
72
.github/workflows/maketplace-plugins-deploy.yml
vendored
72
.github/workflows/maketplace-plugins-deploy.yml
vendored
|
|
@ -10,14 +10,12 @@ env:
|
|||
PR_NUMBER: ${{ github.event.number }}
|
||||
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
||||
|
||||
|
||||
jobs:
|
||||
deploy-marketplace-plugin:
|
||||
if: ${{ github.event.action == 'labeled' && github.event.label.name == 'deploy-marketplace-plugin' }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
- name: Sync repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
|
|
@ -49,28 +47,58 @@ jobs:
|
|||
aws-secret-access-key: ${{ secrets.AWS_SECRET_MAR_ACCESS_KEY }}
|
||||
aws-region: us-east-2
|
||||
|
||||
- name: Install and build dependencies
|
||||
- name: Install and build dependencies in order
|
||||
run: |
|
||||
cd marketplace && npm install && npm run build --workspaces
|
||||
continue-on-error: true
|
||||
|
||||
- name: Build marketplace plugins
|
||||
run: |
|
||||
cd marketplace && AWS_BUCKET=tooljet-plugins-stage node scripts/upload-to-s3.js
|
||||
cd marketplace
|
||||
echo "🔧 Installing all workspace dependencies"
|
||||
npm install
|
||||
|
||||
- name: Comment deployment URL
|
||||
echo "🏗️ Building 'common' plugin first"
|
||||
npm run build --workspace=plugins/common || exit 1
|
||||
|
||||
echo "🔁 Building all remaining plugins"
|
||||
PLUGINS=$(ls plugins | grep -v '^common$')
|
||||
for plugin in $PLUGINS; do
|
||||
echo "🔨 Building plugin: $plugin"
|
||||
npm run build --workspace=plugins/$plugin || exit 1
|
||||
done
|
||||
|
||||
|
||||
- name: Build marketplace plugins and capture summary
|
||||
run: |
|
||||
cd marketplace
|
||||
echo "🚀 Uploading to S3"
|
||||
AWS_BUCKET=tooljet-plugins-stage node scripts/upload-to-s3.js | tee upload_summary.log
|
||||
|
||||
- name: Extract upload summary
|
||||
id: upload_summary
|
||||
run: |
|
||||
SUMMARY=$(awk '/UPLOAD SUMMARY/,0' marketplace/upload_summary.log)
|
||||
echo "UPLOAD_SUMMARY<<EOF" >> $GITHUB_ENV
|
||||
echo "$SUMMARY" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: Comment on success
|
||||
if: success()
|
||||
uses: actions/github-script@v5
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const runId = process.env.GITHUB_RUN_ID;
|
||||
const runUrl = `https://github.com/${{ github.repository }}/actions/runs/${runId}`;
|
||||
const summary = process.env.UPLOAD_SUMMARY;
|
||||
const body = `Marketplace Plugin added to stage bucket\n\n🔍 [View Deployment Logs & Summary](${runUrl})\n\n\`\`\`\n${summary}\n\`\`\``;
|
||||
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: 'Marketplace Plugin added to stage bucket'
|
||||
})
|
||||
body
|
||||
});
|
||||
|
||||
- uses: actions/github-script@v6
|
||||
- name: Label update on success
|
||||
if: success()
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
try {
|
||||
|
|
@ -90,3 +118,19 @@ jobs:
|
|||
repo: context.repo.repo,
|
||||
labels: ['marketplace-plugin-deployed']
|
||||
})
|
||||
|
||||
- name: Comment on failure
|
||||
if: failure()
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const runId = process.env.GITHUB_RUN_ID;
|
||||
const runUrl = `https://github.com/${{ github.repository }}/actions/runs/${runId}`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: `❌ Marketplace Plugin deployment failed.\n\n🔍 [View Deployment Logs & Summary](${runUrl})`
|
||||
});
|
||||
235
.github/workflows/render-preview-deploy.yml
vendored
235
.github/workflows/render-preview-deploy.yml
vendored
|
|
@ -1,7 +1,9 @@
|
|||
name: Render review deploy
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [labeled, unlabeled, closed]
|
||||
types: [labeled, unlabeled, closed, synchronize, opened]
|
||||
issue_comment:
|
||||
types: [created, edited, deleted]
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
||||
|
|
@ -1153,3 +1155,234 @@ jobs:
|
|||
# } catch (e) {
|
||||
# console.log(e)
|
||||
# }
|
||||
|
||||
redeploy-review-app:
|
||||
if: ${{ github.event.action == 'synchronize' || github.event.action == 'opened' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get PR labels
|
||||
id: get_labels
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const labels = await github.rest.issues.listLabelsOnIssue({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number
|
||||
});
|
||||
return labels.data.map(l => l.name);
|
||||
|
||||
- name: Redeploy CE review app if active
|
||||
if: contains(steps.get_labels.outputs.result, 'active-ce-review-app')
|
||||
id: redeploy_ce
|
||||
env:
|
||||
RENDER_API_KEY: ${{ secrets.RENDER_API_KEY }}
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
run: |
|
||||
SERVICE_ID=$(curl --request GET \
|
||||
--url "https://api.render.com/v1/services?name=ToolJet%20CE%20PR%20%23${PR_NUMBER}&limit=1" \
|
||||
--header 'accept: application/json' \
|
||||
--header "authorization: Bearer $RENDER_API_KEY" | jq -r '.[0].service.id')
|
||||
DEPLOY_RESPONSE=$(curl --request POST \
|
||||
--url "https://api.render.com/v1/services/$SERVICE_ID/deploys" \
|
||||
--header 'accept: application/json' \
|
||||
--header 'content-type: application/json' \
|
||||
--header "authorization: Bearer $RENDER_API_KEY" \
|
||||
--data '{"clearCache":"clear"}')
|
||||
DEPLOY_ID=$(echo $DEPLOY_RESPONSE | jq -r '.id')
|
||||
echo "SERVICE_ID=$SERVICE_ID" >> $GITHUB_ENV
|
||||
echo "DEPLOY_ID=$DEPLOY_ID" >> $GITHUB_ENV
|
||||
|
||||
|
||||
- name: Redeploy EE review app if active
|
||||
if: contains(steps.get_labels.outputs.result, 'active-ee-review-app')
|
||||
id: redeploy_ee
|
||||
env:
|
||||
RENDER_API_KEY: ${{ secrets.RENDER_API_KEY }}
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
run: |
|
||||
SERVICE_ID=$(curl --request GET \
|
||||
--url "https://api.render.com/v1/services?name=ToolJet%20EE%20PR%20%23${PR_NUMBER}&limit=1" \
|
||||
--header 'accept: application/json' \
|
||||
--header "authorization: Bearer $RENDER_API_KEY" | jq -r '.[0].service.id')
|
||||
DEPLOY_RESPONSE=$(curl --request POST \
|
||||
--url "https://api.render.com/v1/services/$SERVICE_ID/deploys" \
|
||||
--header 'accept: application/json' \
|
||||
--header 'content-type: application/json' \
|
||||
--header "authorization: Bearer $RENDER_API_KEY" \
|
||||
--data '{"clearCache":"clear"}')
|
||||
DEPLOY_ID=$(echo $DEPLOY_RESPONSE | jq -r '.id')
|
||||
echo "SERVICE_ID=$SERVICE_ID" >> $GITHUB_ENV
|
||||
echo "DEPLOY_ID=$DEPLOY_ID" >> $GITHUB_ENV
|
||||
|
||||
|
||||
render-bot-check-deployment:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.action == 'labeled' && github.event.label.name == 'render-check-deployment'
|
||||
steps:
|
||||
- name: Get PR labels
|
||||
id: get_labels
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const labels = await github.rest.issues.listLabelsOnIssue({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number
|
||||
});
|
||||
return labels.data.map(l => l.name);
|
||||
|
||||
- name: Fetch CE service and deploy ID
|
||||
run: |
|
||||
response=$(curl --silent --request GET \
|
||||
--url "https://api.render.com/v1/services?name=ToolJet%20CE%20PR%20%23${PR_NUMBER}&limit=1" \
|
||||
--header 'accept: application/json' \
|
||||
--header "authorization: Bearer $RENDER_API_KEY")
|
||||
|
||||
SERVICE_ID=$(echo "$response" | jq -r 'if type=="array" and length > 0 then .[0].service.id else empty end')
|
||||
|
||||
if [[ -z "$SERVICE_ID" ]]; then
|
||||
echo "No CE service found for PR #$PR_NUMBER. Skipping deployment ID fetch."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
response_deploy=$(curl --silent --request GET \
|
||||
--url "https://api.render.com/v1/services/$SERVICE_ID/deploys?limit=1" \
|
||||
--header 'accept: application/json' \
|
||||
--header "authorization: Bearer $RENDER_API_KEY")
|
||||
|
||||
DEPLOY_ID=$(echo "$response_deploy" | jq -r 'if type=="array" and length > 0 then .[0].deploy.id else empty end')
|
||||
|
||||
echo "CE_SERVICE_ID=$SERVICE_ID" >> $GITHUB_ENV
|
||||
echo "CE_DEPLOY_ID=$DEPLOY_ID" >> $GITHUB_ENV
|
||||
env:
|
||||
PR_NUMBER: ${{ env.PR_NUMBER }}
|
||||
RENDER_API_KEY: ${{ secrets.RENDER_API_KEY }}
|
||||
|
||||
- name: Comment CE deployment details
|
||||
uses: actions/github-script@v6
|
||||
env:
|
||||
PR_NUMBER: ${{ env.PR_NUMBER }}
|
||||
RENDER_API_KEY: ${{ secrets.RENDER_API_KEY }}
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const prNumber = process.env.PR_NUMBER;
|
||||
const apiKey = process.env.RENDER_API_KEY;
|
||||
|
||||
const ceServiceRes = await fetch(`https://api.render.com/v1/services?name=ToolJet%20CE%20PR%20%23${prNumber}&limit=1`, {
|
||||
headers: {
|
||||
'accept': 'application/json',
|
||||
'authorization': `Bearer ${apiKey}`
|
||||
}
|
||||
});
|
||||
const ceServices = await ceServiceRes.json();
|
||||
const ceServiceId = ceServices[0]?.service?.id || null;
|
||||
|
||||
let ceInfo = 'No active CE review app deployment found.';
|
||||
if (ceServiceId) {
|
||||
const deployRes = await fetch(`https://api.render.com/v1/services/${ceServiceId}/deploys?limit=1`, {
|
||||
headers: {
|
||||
'accept': 'application/json',
|
||||
'authorization': `Bearer ${apiKey}`
|
||||
}
|
||||
});
|
||||
const deployData = await deployRes.json();
|
||||
const deploy = deployData[0]?.deploy || {};
|
||||
const ceCommit = deploy.commit || {};
|
||||
const status = deploy.status || 'unknown';
|
||||
ceInfo = `### Community Edition\n- App: https://tooljet-ce-pr-${prNumber}.onrender.com\n- Dashboard: https://dashboard.render.com/web/${ceServiceId}\n- Commit: ${ceCommit.id || ''}\n- Message: ${ceCommit.message || ''}\n- Status: ${status}`;
|
||||
}
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: ceInfo
|
||||
});
|
||||
|
||||
- name: Fetch EE service and deploy ID
|
||||
run: |
|
||||
response=$(curl --silent --request GET \
|
||||
--url "https://api.render.com/v1/services?name=ToolJet%20EE%20PR%20%23${PR_NUMBER}&limit=1" \
|
||||
--header 'accept: application/json' \
|
||||
--header "authorization: Bearer $RENDER_API_KEY")
|
||||
|
||||
SERVICE_ID=$(echo "$response" | jq -r 'if type=="array" and length > 0 then .[0].service.id else empty end')
|
||||
|
||||
if [[ -z "$SERVICE_ID" ]]; then
|
||||
echo "No EE service found for PR #$PR_NUMBER. Skipping deployment ID fetch."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
response_deploy=$(curl --silent --request GET \
|
||||
--url "https://api.render.com/v1/services/$SERVICE_ID/deploys?limit=1" \
|
||||
--header 'accept: application/json' \
|
||||
--header "authorization: Bearer $RENDER_API_KEY")
|
||||
|
||||
DEPLOY_ID=$(echo "$response_deploy" | jq -r 'if type=="array" and length > 0 then .[0].deploy.id else empty end')
|
||||
|
||||
echo "EE_SERVICE_ID=$SERVICE_ID" >> $GITHUB_ENV
|
||||
echo "EE_DEPLOY_ID=$DEPLOY_ID" >> $GITHUB_ENV
|
||||
env:
|
||||
PR_NUMBER: ${{ env.PR_NUMBER }}
|
||||
RENDER_API_KEY: ${{ secrets.RENDER_API_KEY }}
|
||||
|
||||
- name: Comment EE deployment details
|
||||
uses: actions/github-script@v6
|
||||
env:
|
||||
PR_NUMBER: ${{ env.PR_NUMBER }}
|
||||
RENDER_API_KEY: ${{ secrets.RENDER_API_KEY }}
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const prNumber = process.env.PR_NUMBER;
|
||||
const apiKey = process.env.RENDER_API_KEY;
|
||||
|
||||
const eeServiceRes = await fetch(`https://api.render.com/v1/services?name=ToolJet%20EE%20PR%20%23${prNumber}&limit=1`, {
|
||||
headers: {
|
||||
'accept': 'application/json',
|
||||
'authorization': `Bearer ${apiKey}`
|
||||
}
|
||||
});
|
||||
const eeServices = await eeServiceRes.json();
|
||||
const eeServiceId = eeServices[0]?.service?.id || null;
|
||||
|
||||
let eeInfo = 'No active EE review app deployment found.';
|
||||
if (eeServiceId) {
|
||||
const deployRes = await fetch(`https://api.render.com/v1/services/${eeServiceId}/deploys?limit=1`, {
|
||||
headers: {
|
||||
'accept': 'application/json',
|
||||
'authorization': `Bearer ${apiKey}`
|
||||
}
|
||||
});
|
||||
const deployData = await deployRes.json();
|
||||
const deploy = deployData[0]?.deploy || {};
|
||||
const eeCommit = deploy.commit || {};
|
||||
const status = deploy.status || 'unknown';
|
||||
eeInfo = `### Enterprise Edition\n- App: https://tooljet-ee-pr-${prNumber}.onrender.com\n- Dashboard: https://dashboard.render.com/web/${eeServiceId}\n- Commit: ${eeCommit.id || ''}\n- Message: ${eeCommit.message || ''}\n- Status: ${status}`;
|
||||
}
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: eeInfo
|
||||
});
|
||||
|
||||
- name: Remove label
|
||||
if: contains(steps.get_labels.outputs.result, 'render-check-deployment')
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: 'render-check-deployment'
|
||||
})
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
|
|
|
|||
2
.version
2
.version
|
|
@ -1 +1 @@
|
|||
3.13.0
|
||||
3.14.0
|
||||
|
|
|
|||
114
cypress-tests/cypress-ee-platform.config.js
Normal file
114
cypress-tests/cypress-ee-platform.config.js
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
const { defineConfig } = require("cypress");
|
||||
const { rmdir } = require("fs");
|
||||
const fs = require("fs");
|
||||
const XLSX = require("node-xlsx");
|
||||
const pg = require("pg");
|
||||
const path = require("path");
|
||||
const pdf = require("pdf-parse");
|
||||
|
||||
const environments = {
|
||||
'run-cypress-platform': {
|
||||
baseUrl: "http://localhost:3000",
|
||||
configFile: "cypress-platform.config.js"
|
||||
},
|
||||
'run-cypress-platform-subpath': {
|
||||
baseUrl: "http://localhost:3000/apps",
|
||||
configFile: "cypress-platform.config.js"
|
||||
},
|
||||
'run-cypress-platform-proxy': {
|
||||
baseUrl: "http://localhost:4001",
|
||||
configFile: "cypress-platform.config.js"
|
||||
},
|
||||
'run-cypress-platform-proxy-subpath': {
|
||||
baseUrl: "http://localhost:4001/apps",
|
||||
configFile: "cypress-platform.config.js"
|
||||
}
|
||||
};
|
||||
|
||||
const githubLabel = process.env.GITHUB_LABEL || 'run-cypress-platform';
|
||||
const environment = environments[githubLabel];
|
||||
|
||||
module.exports = defineConfig({
|
||||
execTimeout: 1800000,
|
||||
defaultCommandTimeout: 30000,
|
||||
requestTimeout: 30000,
|
||||
pageLoadTimeout: 30000,
|
||||
responseTimeout: 30000,
|
||||
viewportWidth: 1440,
|
||||
viewportHeight: 960,
|
||||
chromeWebSecurity: false,
|
||||
trashAssetsBeforeRuns: true,
|
||||
e2e: {
|
||||
setupNodeEvents (on, config) {
|
||||
config.baseUrl = environment.baseUrl;
|
||||
|
||||
on("task", {
|
||||
readPdf (pathToPdf) {
|
||||
return new Promise((resolve) => {
|
||||
const pdfPath = path.resolve(pathToPdf);
|
||||
let dataBuffer = fs.readFileSync(pdfPath);
|
||||
pdf(dataBuffer).then(function ({ text }) {
|
||||
resolve(text);
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
on("task", {
|
||||
readXlsx (filePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
let dataBuffer = fs.readFileSync(filePath);
|
||||
const jsonData = XLSX.parse(dataBuffer);
|
||||
resolve(jsonData[0]["data"].toString());
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
on("task", {
|
||||
deleteFolder (folderName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
rmdir(folderName, { maxRetries: 10, recursive: true }, (err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return reject(err);
|
||||
}
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
on("task", {
|
||||
dbConnection ({ dbconfig, sql }) {
|
||||
const client = new pg.Pool(dbconfig);
|
||||
return client.query(sql);
|
||||
},
|
||||
});
|
||||
|
||||
return require("./cypress/plugins/index.js")(on, config);
|
||||
},
|
||||
downloadsFolder: "cypress/downloads",
|
||||
experimentalRunAllSpecs: true,
|
||||
experimentalModfyObstructiveThirdPartyCode: true,
|
||||
baseUrl: environment.baseUrl,
|
||||
configFile: environment.configFile,
|
||||
specPattern: [
|
||||
"cypress/e2e/happyPath/platform/firstUser/firstUserOnboarding.cy.js",
|
||||
"cypress/e2e/happyPath/platform/commonTestcases/**/*.cy.js",
|
||||
"cypress/e2e/happyPath/platform/eeTestcases/**/*.cy.js",
|
||||
],
|
||||
numTestsKeptInMemory: 1,
|
||||
redirectionLimit: 15,
|
||||
experimentalMemoryManagement: true,
|
||||
video: false,
|
||||
videoUploadOnPasses: false,
|
||||
retries: {
|
||||
runMode: 2,
|
||||
openMode: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -8,7 +8,7 @@ const pdf = require("pdf-parse");
|
|||
|
||||
const environments = {
|
||||
'run-cypress-platform': {
|
||||
baseUrl: "http://localhost:8082",
|
||||
baseUrl: "http://localhost:3000",
|
||||
configFile: "cypress-platform.config.js"
|
||||
},
|
||||
'run-cypress-platform-subpath': {
|
||||
|
|
|
|||
189
cypress-tests/cypress.Dockerfile
Normal file
189
cypress-tests/cypress.Dockerfile
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
FROM node:18.18.2-buster AS builder
|
||||
# Fix for JS heap limit allocation issue
|
||||
ENV NODE_OPTIONS="--max-old-space-size=4096"
|
||||
|
||||
RUN mkdir -p /app
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ARG CUSTOM_GITHUB_TOKEN
|
||||
ARG BRANCH_NAME
|
||||
|
||||
# Clone and checkout the frontend repositorys
|
||||
RUN git config --global url."https://x-access-token:${CUSTOM_GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/"
|
||||
|
||||
RUN git config --global http.version HTTP/1.1
|
||||
RUN git config --global http.postBuffer 524288000
|
||||
RUN git clone https://github.com/ToolJet/ToolJet.git .
|
||||
|
||||
# The branch name needs to be changed the branch with modularisation in CE repo
|
||||
RUN git checkout ${BRANCH_NAME}
|
||||
|
||||
RUN git submodule update --init --recursive
|
||||
|
||||
# Checkout the same branch in submodules if it exists, otherwise stay on default branch
|
||||
RUN git submodule foreach 'git checkout ${BRANCH_NAME} || true'
|
||||
|
||||
# Scripts for building
|
||||
COPY ./package.json ./package.json
|
||||
|
||||
# Build plugins
|
||||
COPY ./plugins/package.json ./plugins/package-lock.json ./plugins/
|
||||
RUN npm --prefix plugins install
|
||||
COPY ./plugins/ ./plugins/
|
||||
RUN NODE_ENV=production npm --prefix plugins run build
|
||||
RUN npm --prefix plugins prune --production
|
||||
|
||||
ENV TOOLJET_EDITION=ee
|
||||
|
||||
# Build frontend
|
||||
COPY ./frontend/package.json ./frontend/package-lock.json ./frontend/
|
||||
RUN npm --prefix frontend install
|
||||
COPY ./frontend/ ./frontend/
|
||||
RUN npm --prefix frontend run build --production
|
||||
RUN npm --prefix frontend prune --production
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV TOOLJET_EDITION=ee
|
||||
|
||||
# Build server
|
||||
COPY ./server/package.json ./server/package-lock.json ./server/
|
||||
RUN npm --prefix server install
|
||||
COPY ./server/ ./server/
|
||||
RUN npm install -g @nestjs/cli
|
||||
RUN npm --prefix server run build
|
||||
|
||||
FROM node:18.18.2-bullseye
|
||||
|
||||
RUN apt-get update -yq \
|
||||
&& apt-get install curl wget gnupg zip -yq \
|
||||
&& apt-get install -yq build-essential \
|
||||
&& apt -y install redis \
|
||||
&& apt-get clean -y
|
||||
|
||||
# copy postgrest executable
|
||||
COPY --from=postgrest/postgrest:v12.2.0 /bin/postgrest /bin
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV TOOLJET_EDITION=ee
|
||||
ENV NODE_OPTIONS="--max-old-space-size=4096"
|
||||
RUN apt-get update && apt-get install -y freetds-dev libaio1 wget supervisor
|
||||
|
||||
# Install Instantclient Basic Light Oracle and Dependencies
|
||||
WORKDIR /opt/oracle
|
||||
RUN wget https://tooljet-plugins-production.s3.us-east-2.amazonaws.com/marketplace-assets/oracledb/instantclients/instantclient-basiclite-linuxx64.zip && \
|
||||
wget https://tooljet-plugins-production.s3.us-east-2.amazonaws.com/marketplace-assets/oracledb/instantclients/instantclient-basiclite-linux.x64-11.2.0.4.0.zip && \
|
||||
unzip instantclient-basiclite-linuxx64.zip && rm -f instantclient-basiclite-linuxx64.zip && \
|
||||
unzip instantclient-basiclite-linux.x64-11.2.0.4.0.zip && rm -f instantclient-basiclite-linux.x64-11.2.0.4.0.zip && \
|
||||
cd /opt/oracle/instantclient_21_10 && rm -f *jdbc* *occi* *mysql* *mql1* *ipc1* *jar uidrvci genezi adrci && \
|
||||
cd /opt/oracle/instantclient_11_2 && rm -f *jdbc* *occi* *mysql* *mql1* *ipc1* *jar uidrvci genezi adrci && \
|
||||
echo /opt/oracle/instantclient* > /etc/ld.so.conf.d/oracle-instantclient.conf && ldconfig
|
||||
# Set the Instant Client library paths
|
||||
ENV LD_LIBRARY_PATH="/opt/oracle/instantclient_11_2:/opt/oracle/instantclient_21_10:${LD_LIBRARY_PATH}"
|
||||
|
||||
WORKDIR /
|
||||
|
||||
# copy npm scripts
|
||||
COPY --from=builder /app/package.json ./app/package.json
|
||||
# copy plugins dependencies
|
||||
COPY --from=builder /app/plugins/dist ./app/plugins/dist
|
||||
COPY --from=builder /app/plugins/client.js ./app/plugins/client.js
|
||||
COPY --from=builder /app/plugins/node_modules ./app/plugins/node_modules
|
||||
COPY --from=builder /app/plugins/packages/common ./app/plugins/packages/common
|
||||
COPY --from=builder /app/plugins/package.json ./app/plugins/package.json
|
||||
# copy frontend build
|
||||
COPY --from=builder /app/frontend/build ./app/frontend/build
|
||||
# copy server build
|
||||
COPY --from=builder /app/server/package.json ./app/server/package.json
|
||||
COPY --from=builder /app/server/.version ./app/server/.version
|
||||
COPY --from=builder /app/server/ee/keys ./app/server/ee/keys
|
||||
COPY --from=builder /app/server/node_modules ./app/server/node_modules
|
||||
COPY --from=builder /app/server/templates ./app/server/templates
|
||||
COPY --from=builder /app/server/scripts ./app/server/scripts
|
||||
COPY --from=builder /app/server/dist ./app/server/dist
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 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 apt update && apt -y install postgresql-13 postgresql-client-13 supervisor --fix-missing
|
||||
|
||||
|
||||
# Explicitly create PG main directory with correct ownership
|
||||
RUN mkdir -p /var/lib/postgresql/13/main && \
|
||||
chown -R postgres:postgres /var/lib/postgresql
|
||||
|
||||
RUN mkdir -p /var/log/supervisor /var/run/postgresql && \
|
||||
chown -R postgres:postgres /var/run/postgresql /var/log/supervisor
|
||||
|
||||
# Remove existing data and create directory with proper ownership
|
||||
RUN rm -rf /var/lib/postgresql/13/main && \
|
||||
mkdir -p /var/lib/postgresql/13/main && \
|
||||
chown -R postgres:postgres /var/lib/postgresql
|
||||
|
||||
# Initialize PostgreSQL
|
||||
RUN su - postgres -c "/usr/lib/postgresql/13/bin/initdb -D /var/lib/postgresql/13/main"
|
||||
|
||||
# Configure Supervisor to manage PostgREST, ToolJet, and Redis
|
||||
RUN echo "[supervisord] \n" \
|
||||
"nodaemon=true \n" \
|
||||
"user=root \n" \
|
||||
"\n" \
|
||||
"[program:redis] \n" \
|
||||
"command=redis-server /etc/redis/redis.conf \n" \
|
||||
"user=redis \n" \
|
||||
"autostart=true \n" \
|
||||
"autorestart=true \n" \
|
||||
"stderr_logfile=/var/log/redis/redis-server.log \n" \
|
||||
"stdout_logfile=/var/log/redis/redis-server.log \n" \
|
||||
"\n" \
|
||||
"[program:postgrest] \n" \
|
||||
"command=/bin/postgrest \n" \
|
||||
"autostart=true \n" \
|
||||
"autorestart=true \n" \
|
||||
"\n" \
|
||||
"[program:tooljet] \n" \
|
||||
"user=root \n" \
|
||||
"command=/bin/bash -c '/app/server/scripts/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" | sed 's/ //' > /etc/supervisor/conf.d/supervisord.conf
|
||||
|
||||
# ENV defaults
|
||||
ENV TOOLJET_HOST=http://localhost \
|
||||
PORT=3000 \
|
||||
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=postgres \
|
||||
PG_PASS=postgres \
|
||||
PG_HOST=localhost \
|
||||
ENABLE_TOOLJET_DB=true \
|
||||
TOOLJET_DB_HOST=localhost \
|
||||
TOOLJET_DB_USER=postgres \
|
||||
TOOLJET_DB_PASS=postgres \
|
||||
TOOLJET_DB=tooljet_db \
|
||||
PGRST_HOST=http://localhost:3001 \
|
||||
PGRST_SERVER_PORT=3001 \
|
||||
PGRST_DB_URI=postgres://postgres:postgres@localhost/tooljet_db \
|
||||
PGRST_JWT_SECRET=r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj \
|
||||
PGRST_DB_PRE_CONFIG=postgrest.pre_config \
|
||||
REDIS_HOST=localhost \
|
||||
REDIS_PORT=6379 \
|
||||
REDIS_USER= \
|
||||
REDIS_PASSWORD= \
|
||||
ORM_LOGGING=true \
|
||||
DEPLOYMENT_PLATFORM=docker:local \
|
||||
HOME=/home/appuser \
|
||||
TERM=xterm
|
||||
|
||||
|
||||
RUN chmod +x ./server/scripts/preview.sh
|
||||
# Set the entrypoint
|
||||
ENTRYPOINT ["./server/scripts/preview.sh"]
|
||||
|
|
@ -52,7 +52,7 @@ Cypress.Commands.add("apiCreateGDS", (url, name, kind, options) => {
|
|||
log: false;
|
||||
}
|
||||
expect(response.status).to.equal(201);
|
||||
Cypress.env(`${kind}`, response.body.id);
|
||||
Cypress.env(`${name}`, response.body.id);
|
||||
|
||||
Cypress.log({
|
||||
name: "Create Data Source",
|
||||
|
|
@ -80,13 +80,14 @@ Cypress.Commands.add("apiFetchDataSourcesId", () => {
|
|||
Cypress.log({
|
||||
name: "DS Fetch",
|
||||
displayName: "Data Sources Fetched",
|
||||
message: dataSources.map(ds => `\nKind: '${ds.kind}', Name: '${ds.id}'`).join(','),
|
||||
message: dataSources
|
||||
.map((ds) => `\nKind: '${ds.kind}', Name: '${ds.id}'`)
|
||||
.join(","),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Cypress.Commands.add("apiCreateApp", (appName = "testApp") => {
|
||||
cy.window({ log: false }).then((win) => {
|
||||
win.localStorage.setItem("walkthroughCompleted", "true");
|
||||
|
|
@ -168,7 +169,6 @@ Cypress.Commands.add(
|
|||
|
||||
Cypress.env("editingVersionId", responseData.editing_version.id);
|
||||
Cypress.env("environmentId", responseData.editorEnvironment.id);
|
||||
|
||||
});
|
||||
cy.get(componentSelector, { timeout: 10000 });
|
||||
}
|
||||
|
|
@ -221,21 +221,21 @@ Cypress.Commands.add(
|
|||
const requestBody =
|
||||
envVar === "Enterprise"
|
||||
? {
|
||||
email: userEmail,
|
||||
firstName: userName,
|
||||
groups: [],
|
||||
lastName: "",
|
||||
role: userRole,
|
||||
userMetadata: metaData,
|
||||
}
|
||||
email: userEmail,
|
||||
firstName: userName,
|
||||
groups: [],
|
||||
lastName: "",
|
||||
role: userRole,
|
||||
userMetadata: metaData,
|
||||
}
|
||||
: {
|
||||
email: userEmail,
|
||||
firstName: userName,
|
||||
groups: [],
|
||||
lastName: "",
|
||||
role: userRole,
|
||||
userMetadata: metaData,
|
||||
};
|
||||
email: userEmail,
|
||||
firstName: userName,
|
||||
groups: [],
|
||||
lastName: "",
|
||||
role: userRole,
|
||||
userMetadata: metaData,
|
||||
};
|
||||
|
||||
cy.getCookie("tj_auth_token").then((cookie) => {
|
||||
cy.request(
|
||||
|
|
@ -289,7 +289,9 @@ Cypress.Commands.add("apiAddQuery", (queryName, query, dataQueryId) => {
|
|||
Cypress.Commands.add(
|
||||
"apiAddQueryToApp",
|
||||
(queryName, options, dsName, dsKind) => {
|
||||
cy.log(`${Cypress.env("server_host")}/api/data-queries/data-sources/${Cypress.env(dsKind)}/versions/${Cypress.env("editingVersionId")}`)
|
||||
cy.log(
|
||||
`${Cypress.env("server_host")}/api/data-queries/data-sources/${Cypress.env(dsKind)}/versions/${Cypress.env("editingVersionId")}`
|
||||
);
|
||||
cy.getCookie("tj_auth_token", { log: false }).then((cookie) => {
|
||||
const authToken = `tj_auth_token=${cookie.value}`;
|
||||
const workspaceId = Cypress.env("workspaceId");
|
||||
|
|
@ -737,3 +739,55 @@ Cypress.Commands.add("apiGetAppData", (appId = Cypress.env("appId")) => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("apiDeleteGDS", (name) => {
|
||||
const dataSourceId = Cypress.env(`${name}`);
|
||||
|
||||
cy.getCookie("tj_auth_token").then((cookie) => {
|
||||
cy.request({
|
||||
method: "DELETE",
|
||||
url: `${Cypress.env("server_host")}/api/data-sources/${dataSourceId}`,
|
||||
headers: {
|
||||
"Tj-Workspace-Id": Cypress.env("workspaceId"),
|
||||
Cookie: `tj_auth_token=${cookie.value}`,
|
||||
},
|
||||
failOnStatusCode: false,
|
||||
}).then((response) => {
|
||||
console.log("Delete response:", response);
|
||||
|
||||
expect(response.status, "Delete status code").to.eq(200);
|
||||
|
||||
Cypress.log({
|
||||
name: "Delete Data Source",
|
||||
displayName: "Data source deleted",
|
||||
message: `Name: '${name}' | ID: '${dataSourceId}'`,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add(
|
||||
"apiUpdateGDS",
|
||||
({ name, options, envName = "development" }) => {
|
||||
cy.getAuthHeaders().then((headers) => {
|
||||
cy.apiGetEnvironments().then((environments) => {
|
||||
const environment = environments.find((env) => env.name === envName);
|
||||
const environmentId = environment.id;
|
||||
const dataSourceId = Cypress.env(`${name}`);
|
||||
|
||||
cy.request({
|
||||
method: "PUT",
|
||||
url: `${Cypress.env("server_host")}/api/data-sources/${dataSourceId}?environment_id=${environmentId}`,
|
||||
headers: headers,
|
||||
body: {
|
||||
name: name,
|
||||
options: options,
|
||||
},
|
||||
}).then((response) => {
|
||||
expect(response.status).to.equal(200);
|
||||
cy.log(`Datasource "${name}" updated successfully.`);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -226,9 +226,9 @@ Cypress.Commands.add(
|
|||
.invoke("text")
|
||||
.then((text) => {
|
||||
cy.wrap(subject).realType(createBackspaceText(text)),
|
||||
{
|
||||
delay: 0,
|
||||
};
|
||||
{
|
||||
delay: 0,
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
@ -429,7 +429,6 @@ Cypress.Commands.add("visitSlug", ({ actualUrl }) => {
|
|||
});
|
||||
});
|
||||
|
||||
|
||||
Cypress.Commands.add("releaseApp", () => {
|
||||
if (Cypress.env("environment") !== "Community") {
|
||||
cy.get(commonEeSelectors.promoteButton).click();
|
||||
|
|
@ -549,7 +548,7 @@ Cypress.Commands.add("installMarketplacePlugin", (pluginName) => {
|
|||
}
|
||||
});
|
||||
|
||||
function installPlugin (pluginName) {
|
||||
function installPlugin(pluginName) {
|
||||
cy.get('[data-cy="-list-item"]').eq(1).click();
|
||||
cy.wait(1000);
|
||||
|
||||
|
|
@ -605,3 +604,20 @@ Cypress.Commands.add("uninstallMarketplacePlugin", (pluginName) => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add(
|
||||
"verifyRequiredFieldValidation",
|
||||
(fieldName, expectedColor) => {
|
||||
cy.get(commonSelectors.textField(fieldName)).should(
|
||||
"have.css",
|
||||
"border-color",
|
||||
expectedColor
|
||||
);
|
||||
cy.get(commonSelectors.labelFieldValidation(fieldName))
|
||||
.should("be.visible")
|
||||
.and("have.text", `${fieldName} is required`);
|
||||
cy.get(commonSelectors.labelFieldAlert(fieldName))
|
||||
.should("be.visible")
|
||||
.and("have.text", `${fieldName} is required`);
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export const cyParamName = (paramName = "") => {
|
||||
return paramName.toLowerCase().replace(/\s+/g, "-");
|
||||
return String(paramName).toLowerCase().replace(/\s+/g, "-");
|
||||
};
|
||||
|
||||
export const commonSelectors = {
|
||||
|
|
@ -278,6 +278,16 @@ export const commonSelectors = {
|
|||
defaultModalTitle: '[data-cy="modal-title"]',
|
||||
workspaceConstantsIcon: '[data-cy="icon-workspace-constants"]',
|
||||
confirmationButton: '[data-cy="confirmation-button"]',
|
||||
|
||||
textField: (fieldName) => {
|
||||
return `[data-cy="${cyParamName(fieldName)}-text-field"]`;
|
||||
},
|
||||
labelFieldValidation: (fieldName) => {
|
||||
return `[data-cy="${cyParamName(fieldName)}-is-required-validation-label"]`;
|
||||
},
|
||||
labelFieldAlert: (fieldName) => {
|
||||
return `[data-cy="${cyParamName(fieldName)}-is-required-field-alert-text"]`;
|
||||
},
|
||||
};
|
||||
|
||||
export const commonWidgetSelector = {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export const dataSourceSelector = {
|
|||
buttonTestConnection: '[data-cy="test-connection-button"]',
|
||||
connectionFailedText: '[data-cy="test-connection-failed-text"]',
|
||||
buttonSave: '[data-cy="db-connection-save-button"] > .tj-base-btn',
|
||||
dangerAlertNotSupportSSL: '.go3958317564',
|
||||
dangerAlertNotSupportSSL: ".go3958317564",
|
||||
|
||||
passwordTextField: '[data-cy="password-text-field"]',
|
||||
textConnectionVerified: '[data-cy="test-connection-verified-text"]',
|
||||
|
|
@ -101,7 +101,48 @@ export const dataSourceSelector = {
|
|||
unSavedModalTitle: '[data-cy="unsaved-changes-title"]',
|
||||
eventQuerySelectionField: '[data-cy="query-selection-field"]',
|
||||
connectionAlertText: '[data-cy="connection-alert-text"]',
|
||||
requiredIndicator: '[data-cy="required-indicator"]',
|
||||
informationIcon: '[data-cy="information-icon"]',
|
||||
deleteDSButton: (datasourceName) => {
|
||||
return `[data-cy="${cyParamName(datasourceName)}-delete-button"]`
|
||||
return `[data-cy="${cyParamName(datasourceName)}-delete-button"]`;
|
||||
},
|
||||
labelFieldName: (fieldName) => {
|
||||
return `[data-cy="${cyParamName(fieldName)}-field-label"]`;
|
||||
},
|
||||
dataSourceNameButton: (dataSourceName) => {
|
||||
return `[data-cy="${cyParamName(dataSourceName)}-button"]`;
|
||||
},
|
||||
dropdownLabel: (label) => {
|
||||
return `[data-cy="${cyParamName(label)}-dropdown-label"]`;
|
||||
},
|
||||
textField: (fieldName) => {
|
||||
return `[data-cy="${cyParamName(fieldName)}-text-field"]`;
|
||||
},
|
||||
subSection: (header) => {
|
||||
return `[data-cy="${cyParamName(header)}-section"]`;
|
||||
},
|
||||
toggleInput: (toggleName) => {
|
||||
return `[data-cy="${cyParamName(toggleName)}-toggle-input"]`;
|
||||
},
|
||||
button: (buttonName) => {
|
||||
return `[data-cy="button-${cyParamName(buttonName)}"]`;
|
||||
},
|
||||
keyInputField: (header, index) => {
|
||||
return `[data-cy="${cyParamName(header)}-key-input-field-${cyParamName(index)}"]`;
|
||||
},
|
||||
valueInputField: (header, index) => {
|
||||
return `[data-cy="${cyParamName(header)}-value-input-field-${cyParamName(index)}"]`;
|
||||
},
|
||||
deleteButton: (header, index) => {
|
||||
return `[data-cy="${cyParamName(header)}-delete-button-${cyParamName(index)}"]`;
|
||||
},
|
||||
addMoreButton: (header) => {
|
||||
return `[data-cy="${cyParamName(header)}-add-button"]`;
|
||||
},
|
||||
dropdownField: (fieldName) => {
|
||||
return `[data-cy="${cyParamName(fieldName)}-select-dropdown"]`;
|
||||
},
|
||||
labelFieldValidation: (fieldName) => {
|
||||
return `[data-cy="${cyParamName(fieldName)}-is-required-validation-label"]`;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -87,6 +87,8 @@ export const postgreSqlSelector = {
|
|||
recordsInputField: '[data-cy="records-input-field"]',
|
||||
|
||||
eventQuerySelectionField: '[data-cy="query-selection-field"]',
|
||||
sslToggleInput: '[data-cy="ssl-enabled-toggle-input"]',
|
||||
labelEncryptedText: '[data-cy="encrypted-text"]',
|
||||
};
|
||||
|
||||
export const airTableSelector = {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
export const airtableText = {
|
||||
airtable: "Airtable",
|
||||
cypressairtable: "cypress-Airtable",
|
||||
ApiKey: "Personal access token",
|
||||
apikeyPlaceholder: "**************",
|
||||
};
|
||||
airtable: "Airtable",
|
||||
cypressairtable: "cypress-Airtable",
|
||||
ApiKey: "Personal access token",
|
||||
apikeyPlaceholder: "**************",
|
||||
invalidAccessToken: "Authentication failed: Invalid personal access token",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -17,13 +17,17 @@ export const postgreSqlText = {
|
|||
allCloudStorage: "Cloud Storages (4)",
|
||||
|
||||
postgreSQL: "PostgreSQL",
|
||||
labelConnectionType: "Connection type",
|
||||
manualConnectionOption: "Manual connection",
|
||||
connectionStringOption: "Connection string",
|
||||
labelHost: "Host",
|
||||
labelPort: "Port",
|
||||
labelSSL: "SSL",
|
||||
labelDbName: "Database name",
|
||||
labelUserName: "Username",
|
||||
labelPassword: "Password",
|
||||
label: "Encrypted",
|
||||
labelEncrypted: "Encrypted",
|
||||
labelConnectionOptions: "Connection options",
|
||||
sslCertificate: "SSL certificate",
|
||||
whiteListIpText:
|
||||
"Please white-list our IP address if the data source is not publicly accessible",
|
||||
|
|
@ -74,6 +78,8 @@ export const postgreSqlText = {
|
|||
|
||||
guiOptionBulkUpdate: "Bulk update using primary key",
|
||||
buttonTextTestConnection: "Test connection",
|
||||
editButtonText: "Edit",
|
||||
unableAcquireConnectionAlertText: "Unable to acquire a connection",
|
||||
|
||||
tabAdvanced: "Advanced",
|
||||
labelNoEventhandler: "No event handlers",
|
||||
|
|
|
|||
|
|
@ -3,24 +3,15 @@ import { postgreSqlSelector, airTableSelector } from "Selectors/postgreSql";
|
|||
import { postgreSqlText } from "Texts/postgreSql";
|
||||
import { airtableText } from "Texts/airTable";
|
||||
import { commonSelectors } from "Selectors/common";
|
||||
import { commonText } from "Texts/common";
|
||||
|
||||
import {
|
||||
fillDataSourceTextField,
|
||||
selectAndAddDataSource,
|
||||
} from "Support/utils/postgreSql";
|
||||
|
||||
import {
|
||||
deleteDatasource,
|
||||
closeDSModal,
|
||||
deleteAppandDatasourceAfterExecution,
|
||||
} from "Support/utils/dataSource";
|
||||
|
||||
import { closeDSModal } from "Support/utils/dataSource";
|
||||
import { dataSourceSelector } from "../../../../../constants/selectors/dataSource";
|
||||
|
||||
const data = {};
|
||||
|
||||
data.queryName = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", "");
|
||||
const airTable_apiKey = Cypress.env("airTable_apikey");
|
||||
const airTable_baseId = Cypress.env("airtabelbaseId");
|
||||
const airTable_tableName = Cypress.env("airtable_tableName");
|
||||
const airTable_recordID = Cypress.env("airtable_recordId");
|
||||
|
||||
describe("Data source Airtable", () => {
|
||||
beforeEach(() => {
|
||||
|
|
@ -54,72 +45,142 @@ describe("Data source Airtable", () => {
|
|||
postgreSqlText.allCloudStorage
|
||||
);
|
||||
|
||||
selectAndAddDataSource("databases", airtableText.airtable, data.dsName);
|
||||
|
||||
cy.get(postgreSqlSelector.buttonSave).verifyVisibleElement(
|
||||
cy.apiCreateGDS(
|
||||
`${Cypress.env("server_host")}/api/data-sources`,
|
||||
`cypress-${data.dsName}-airtable`,
|
||||
"airtable",
|
||||
[
|
||||
{
|
||||
key: "personal_access_token",
|
||||
value: `${Cypress.env("airTable_apikey")}`,
|
||||
encrypted: true,
|
||||
},
|
||||
]
|
||||
);
|
||||
cy.reload();
|
||||
cy.get(
|
||||
dataSourceSelector.dataSourceNameButton(`cypress-${data.dsName}-airtable`)
|
||||
)
|
||||
.should("be.visible")
|
||||
.click();
|
||||
cy.get(
|
||||
dataSourceSelector.labelFieldName(airtableText.ApiKey)
|
||||
).verifyVisibleElement("have.text", `${airtableText.ApiKey}*`);
|
||||
cy.get(postgreSqlSelector.labelEncryptedText).verifyVisibleElement(
|
||||
"have.text",
|
||||
postgreSqlText.buttonTextSave
|
||||
postgreSqlText.labelEncrypted
|
||||
);
|
||||
cy.get(dataSourceSelector.button(postgreSqlText.editButtonText)).should(
|
||||
"be.visible"
|
||||
);
|
||||
cy.get(dataSourceSelector.button(postgreSqlText.editButtonText)).click();
|
||||
cy.verifyRequiredFieldValidation(airtableText.ApiKey, "rgb(226, 99, 103)");
|
||||
cy.get(dataSourceSelector.textField(airtableText.ApiKey)).should(
|
||||
"be.visible"
|
||||
);
|
||||
cy.get(postgreSqlSelector.labelIpWhitelist).verifyVisibleElement(
|
||||
"have.text",
|
||||
postgreSqlText.whiteListIpText
|
||||
);
|
||||
cy.get(postgreSqlSelector.buttonCopyIp).verifyVisibleElement(
|
||||
"have.text",
|
||||
postgreSqlText.textCopy
|
||||
);
|
||||
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
postgreSqlText.toastDSSaved
|
||||
cy.get(postgreSqlSelector.linkReadDocumentation).verifyVisibleElement(
|
||||
"have.text",
|
||||
postgreSqlText.readDocumentation
|
||||
);
|
||||
deleteDatasource(`cypress-${data.dsName}-airtable`);
|
||||
cy.get(postgreSqlSelector.buttonTestConnection)
|
||||
.verifyVisibleElement(
|
||||
"have.text",
|
||||
postgreSqlText.buttonTextTestConnection
|
||||
)
|
||||
.click();
|
||||
cy.get(postgreSqlSelector.connectionFailedText).verifyVisibleElement(
|
||||
"have.text",
|
||||
postgreSqlText.couldNotConnect
|
||||
);
|
||||
cy.get(postgreSqlSelector.buttonSave)
|
||||
.verifyVisibleElement("have.text", postgreSqlText.buttonTextSave)
|
||||
.and("be.disabled");
|
||||
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
|
||||
"have.text",
|
||||
airtableText.invalidAccessToken
|
||||
);
|
||||
|
||||
cy.apiDeleteGDS(`cypress-${data.dsName}-airtable`);
|
||||
});
|
||||
|
||||
it("Should verify the functionality of AirTable connection form.", () => {
|
||||
selectAndAddDataSource("databases", airtableText.airtable, data.dsName);
|
||||
|
||||
fillDataSourceTextField(
|
||||
airtableText.ApiKey,
|
||||
airtableText.apikeyPlaceholder,
|
||||
Cypress.env("airTable_apikey")
|
||||
);
|
||||
cy.get(postgreSqlSelector.buttonSave).click();
|
||||
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
postgreSqlText.toastDSSaved
|
||||
);
|
||||
|
||||
cy.get(commonSelectors.globalDataSourceIcon).click();
|
||||
cy.apiCreateGDS(
|
||||
`${Cypress.env("server_host")}/api/data-sources`,
|
||||
`cypress-${data.dsName}-airtable`,
|
||||
"airtable",
|
||||
[
|
||||
{
|
||||
key: "personal_access_token",
|
||||
value: "Invalid access token",
|
||||
encrypted: true,
|
||||
},
|
||||
]
|
||||
);
|
||||
cy.get(
|
||||
`[data-cy="cypress-${data.dsName}-airtable-button"]`
|
||||
).verifyVisibleElement("have.text", `cypress-${data.dsName}-airtable`);
|
||||
deleteDatasource(`cypress-${data.dsName}-airtable`);
|
||||
dataSourceSelector.dataSourceNameButton(`cypress-${data.dsName}-airtable`)
|
||||
)
|
||||
.should("be.visible")
|
||||
.click();
|
||||
|
||||
cy.get(postgreSqlSelector.buttonTestConnection).click();
|
||||
cy.get(dataSourceSelector.connectionFailedText, {
|
||||
timeout: 10000,
|
||||
}).should("have.text", postgreSqlText.couldNotConnect);
|
||||
|
||||
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
|
||||
"have.text",
|
||||
airtableText.invalidAccessToken
|
||||
);
|
||||
cy.reload();
|
||||
cy.apiUpdateGDS({
|
||||
name: `cypress-${data.dsName}-airtable`,
|
||||
options: [
|
||||
{
|
||||
key: "personal_access_token",
|
||||
value: `${Cypress.env("airTable_apikey")}`,
|
||||
encrypted: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
cy.get(
|
||||
dataSourceSelector.dataSourceNameButton(`cypress-${data.dsName}-airtable`)
|
||||
)
|
||||
.should("be.visible")
|
||||
.click();
|
||||
|
||||
cy.get(postgreSqlSelector.buttonTestConnection).click();
|
||||
cy.get(postgreSqlSelector.textConnectionVerified, {
|
||||
timeout: 10000,
|
||||
}).should("have.text", postgreSqlText.labelConnectionVerified);
|
||||
|
||||
cy.apiDeleteGDS(`cypress-${data.dsName}-airtable`);
|
||||
});
|
||||
|
||||
it("Should able to run the query with valid conection", () => {
|
||||
const airTable_apiKey = Cypress.env("airTable_apikey");
|
||||
const airTable_baseId = Cypress.env("airtabelbaseId");
|
||||
const airTable_tableName = Cypress.env("airtable_tableName");
|
||||
const airTable_recordID = Cypress.env("airtable_recordId");
|
||||
|
||||
selectAndAddDataSource("databases", airtableText.airtable, data.dsName);
|
||||
|
||||
fillDataSourceTextField(
|
||||
airtableText.ApiKey,
|
||||
airtableText.apikeyPlaceholder,
|
||||
airTable_apiKey
|
||||
cy.apiCreateGDS(
|
||||
`${Cypress.env("server_host")}/api/data-sources`,
|
||||
`cypress-${data.dsName}-airtable`,
|
||||
"airtable",
|
||||
[
|
||||
{
|
||||
key: "personal_access_token",
|
||||
value: `${airTable_apiKey}`,
|
||||
encrypted: true,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
cy.wait(1000);
|
||||
cy.get(postgreSqlSelector.buttonSave).click();
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
postgreSqlText.toastDSSaved
|
||||
);
|
||||
|
||||
cy.get(commonSelectors.globalDataSourceIcon).click();
|
||||
cy.get(
|
||||
`[data-cy="cypress-${data.dsName}-airtable-button"]`
|
||||
).verifyVisibleElement("have.text", `cypress-${data.dsName}-airtable`);
|
||||
cy.get(commonSelectors.dashboardIcon).click();
|
||||
cy.get(commonSelectors.appCreateButton).click();
|
||||
cy.get(commonSelectors.appNameInput).click().type(data.dsName);
|
||||
cy.get(commonSelectors.createAppButton).click();
|
||||
cy.skipWalkthrough();
|
||||
cy.apiCreateApp(`${data.dsName}-airtable-app`);
|
||||
cy.openApp();
|
||||
|
||||
cy.get('[data-cy="show-ds-popover-button"]').click();
|
||||
cy.get(".css-4e90k9").type(`${data.dsName}`);
|
||||
|
|
@ -280,10 +341,9 @@ describe("Data source Airtable", () => {
|
|||
commonSelectors.toastMessage,
|
||||
`Query (${data.queryName}) completed.`
|
||||
);
|
||||
deleteAppandDatasourceAfterExecution(
|
||||
data.dsName,
|
||||
`cypress-${data.dsName}-airtable`
|
||||
);
|
||||
|
||||
cy.apiDeleteApp(`${data.dsName}-airtable-app`);
|
||||
cy.apiDeleteGDS(`cypress-${data.dsName}-airtable`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -66,10 +66,9 @@ describe("Data source BigQuery", () => {
|
|||
`cypress-${data.dataSourceName}-bigquery`
|
||||
);
|
||||
|
||||
cy.get('[data-cy="label-private-key"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
firestoreText.labelPrivateKey
|
||||
);
|
||||
cy.get(
|
||||
dataSourceSelector.labelFieldName(firestoreText.labelPrivateKey)
|
||||
).verifyVisibleElement("have.text", "Private key*");
|
||||
cy.get(".datasource-edit-btn").should("be.visible");
|
||||
cy.get(postgreSqlSelector.labelIpWhitelist).verifyVisibleElement(
|
||||
"have.text",
|
||||
|
|
@ -98,7 +97,7 @@ describe("Data source BigQuery", () => {
|
|||
"have.text",
|
||||
postgreSqlText.buttonTextSave
|
||||
);
|
||||
cy.get('[data-cy="connection-alert-text"]').verifyVisibleElement(
|
||||
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
|
||||
"have.text",
|
||||
bigqueryText.errorInvalidEmailId
|
||||
);
|
||||
|
|
@ -110,38 +109,30 @@ describe("Data source BigQuery", () => {
|
|||
});
|
||||
|
||||
it("Should verify the functionality of BigQuery connection form.", () => {
|
||||
selectAndAddDataSource(
|
||||
"databases",
|
||||
bigqueryText.bigQuery,
|
||||
data.dataSourceName
|
||||
);
|
||||
|
||||
fillDataSourceTextField(
|
||||
firestoreText.privateKey,
|
||||
bigqueryText.placehlderPrivateKey,
|
||||
`${JSON.stringify(Cypress.env("bigquery_pvt_key"))}`,
|
||||
"contain",
|
||||
{ parseSpecialCharSequences: false, delay: 0 }
|
||||
cy.get(commonSelectors.globalDataSourceIcon).click();
|
||||
cy.apiCreateGDS(
|
||||
`${Cypress.env("server_host")}/api/data-sources`,
|
||||
`cypress-${data.dataSourceName}-bigquery`,
|
||||
"bigquery",
|
||||
[
|
||||
{
|
||||
key: "private_key",
|
||||
value: `${JSON.stringify(Cypress.env("bigquery_pvt_key"))}`,
|
||||
encrypted: true,
|
||||
},
|
||||
]
|
||||
);
|
||||
cy.get(
|
||||
dataSourceSelector.dataSourceNameButton(
|
||||
`cypress-${data.dataSourceName}-bigquery`
|
||||
)
|
||||
)
|
||||
.should("be.visible")
|
||||
.click();
|
||||
cy.get(postgreSqlSelector.buttonTestConnection).click();
|
||||
cy.get(postgreSqlSelector.textConnectionVerified, {
|
||||
timeout: 10000,
|
||||
}).should("have.text", postgreSqlText.labelConnectionVerified);
|
||||
cy.get(postgreSqlSelector.buttonSave).click();
|
||||
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
postgreSqlText.toastDSSaved
|
||||
);
|
||||
|
||||
cy.get(commonSelectors.globalDataSourceIcon).click();
|
||||
cy.get(
|
||||
`[data-cy="cypress-${data.dataSourceName}-bigquery-button"]`
|
||||
).verifyVisibleElement(
|
||||
"have.text",
|
||||
`cypress-${data.dataSourceName}-bigquery`
|
||||
);
|
||||
|
||||
deleteDatasource(`cypress-${data.dataSourceName}-bigquery`);
|
||||
cy.apiDeleteGDS(`cypress-${data.dataSourceName}-bigquery`);
|
||||
});
|
||||
});
|
||||
|
|
@ -101,7 +101,7 @@ describe("Data sources", () => {
|
|||
"have.text",
|
||||
postgreSqlText.buttonTextSave
|
||||
);
|
||||
cy.get('[data-cy="connection-alert-text"]').verifyVisibleElement(
|
||||
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
|
||||
"have.text",
|
||||
"Invalid URL"
|
||||
);
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ describe("Data sources", () => {
|
|||
"have.text",
|
||||
postgreSqlText.buttonTextSave
|
||||
);
|
||||
cy.get('[data-cy="connection-alert-text"]').verifyVisibleElement(
|
||||
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
|
||||
"have.text",
|
||||
"Invalid URL"
|
||||
);
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ describe("Data source DynamoDB", () => {
|
|||
"have.text",
|
||||
postgreSqlText.buttonTextSave
|
||||
);
|
||||
cy.get('[data-cy="connection-alert-text"]').verifyVisibleElement(
|
||||
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
|
||||
"have.text",
|
||||
dynamoDbText.errorMissingRegion
|
||||
);
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ describe("Data source Elasticsearch", () => {
|
|||
"have.text",
|
||||
postgreSqlText.buttonTextSave
|
||||
);
|
||||
cy.get('[data-cy="connection-alert-text"]').verifyVisibleElement(
|
||||
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
|
||||
"have.text",
|
||||
elasticsearchText.errorConnectionRefused
|
||||
);
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ describe("Data source Firestore", () => {
|
|||
"have.text",
|
||||
postgreSqlText.buttonTextSave
|
||||
);
|
||||
cy.get('[data-cy="connection-alert-text"]').verifyVisibleElement(
|
||||
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
|
||||
"have.text",
|
||||
firestoreText.errorGcpKeyCouldNotBeParsed
|
||||
);
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ describe("Data sources", () => {
|
|||
"have.text",
|
||||
postgreSqlText.buttonTextSave
|
||||
);
|
||||
cy.get('[data-cy="connection-alert-text"]').verifyVisibleElement(
|
||||
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
|
||||
"have.text",
|
||||
"Invalid URL"
|
||||
);
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ describe("Data sources", () => {
|
|||
"have.text",
|
||||
postgreSqlText.buttonTextSave
|
||||
);
|
||||
// cy.get('[data-cy="connection-alert-text"]').should("be.visible")
|
||||
// cy.get(dataSourceSelector.connectionAlertText).should("be.visible")
|
||||
deleteDatasource(`cypress-${data.dataSourceName}-mariadb`);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ describe("Data source MongoDB", () => {
|
|||
"have.text",
|
||||
postgreSqlText.buttonTextSave
|
||||
);
|
||||
cy.get('[data-cy="connection-alert-text"]').verifyVisibleElement(
|
||||
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
|
||||
"have.text",
|
||||
mongoDbText.errorConnectionRefused
|
||||
);
|
||||
|
|
@ -164,7 +164,7 @@ describe("Data source MongoDB", () => {
|
|||
}).verifyVisibleElement("have.text", postgreSqlText.couldNotConnect, {
|
||||
timeout: 95000,
|
||||
});
|
||||
cy.get('[data-cy="connection-alert-text"]').verifyVisibleElement(
|
||||
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
|
||||
"have.text",
|
||||
"Cannot read properties of null (reading '2')"
|
||||
);
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ describe("Data sources", () => {
|
|||
.replaceAll("[^A-Za-z]", "");
|
||||
});
|
||||
|
||||
it.skip("Should verify elements on connection form", () => {
|
||||
it("Should verify elements on connection form with validation", () => {
|
||||
cy.log(process.env.NODE_ENV);
|
||||
cy.log(postgreSqlText.allDatabase());
|
||||
cy.get(commonSelectors.globalDataSourceIcon).click();
|
||||
|
|
@ -81,30 +81,147 @@ describe("Data sources", () => {
|
|||
`cypress-${data.dataSourceName}-postgresql`
|
||||
);
|
||||
|
||||
cy.get(postgreSqlSelector.labelHost).verifyVisibleElement(
|
||||
cy.get(
|
||||
dataSourceSelector.dropdownLabel(postgreSqlText.labelConnectionType)
|
||||
).verifyVisibleElement("have.text", postgreSqlText.labelConnectionType);
|
||||
cy.get(dataSourceSelector.dropdownField(postgreSqlText.labelConnectionType))
|
||||
.should("be.visible")
|
||||
.click();
|
||||
cy.contains(
|
||||
`[id*="react-select-"]`,
|
||||
postgreSqlText.connectionStringOption
|
||||
).click();
|
||||
|
||||
cy.get(
|
||||
dataSourceSelector.dropdownField(postgreSqlText.labelConnectionType)
|
||||
).should("be.visible");
|
||||
cy.get(
|
||||
dataSourceSelector.labelFieldName(postgreSqlText.connectionStringOption)
|
||||
).verifyVisibleElement(
|
||||
"have.text",
|
||||
postgreSqlText.labelHost
|
||||
`${postgreSqlText.connectionStringOption}*`
|
||||
);
|
||||
cy.get(postgreSqlSelector.labelPort).verifyVisibleElement(
|
||||
cy.get(postgreSqlSelector.labelEncryptedText).verifyVisibleElement(
|
||||
"have.text",
|
||||
postgreSqlText.labelPort
|
||||
postgreSqlText.labelEncrypted
|
||||
);
|
||||
cy.get(dataSourceSelector.button(postgreSqlText.editButtonText)).should(
|
||||
"be.visible"
|
||||
);
|
||||
cy.get(dataSourceSelector.button(postgreSqlText.editButtonText)).click();
|
||||
cy.verifyRequiredFieldValidation(
|
||||
postgreSqlText.connectionStringOption,
|
||||
"rgb(226, 99, 103)"
|
||||
);
|
||||
cy.get(
|
||||
dataSourceSelector.textField(postgreSqlText.connectionStringOption)
|
||||
).should("be.visible");
|
||||
cy.get(postgreSqlSelector.labelIpWhitelist).verifyVisibleElement(
|
||||
"have.text",
|
||||
postgreSqlText.whiteListIpText
|
||||
);
|
||||
cy.get(postgreSqlSelector.buttonCopyIp).verifyVisibleElement(
|
||||
"have.text",
|
||||
postgreSqlText.textCopy
|
||||
);
|
||||
|
||||
cy.get(postgreSqlSelector.linkReadDocumentation).verifyVisibleElement(
|
||||
"have.text",
|
||||
postgreSqlText.readDocumentation
|
||||
);
|
||||
cy.get(postgreSqlSelector.buttonTestConnection)
|
||||
.verifyVisibleElement(
|
||||
"have.text",
|
||||
postgreSqlText.buttonTextTestConnection
|
||||
)
|
||||
.click();
|
||||
cy.get(postgreSqlSelector.connectionFailedText).verifyVisibleElement(
|
||||
"have.text",
|
||||
postgreSqlText.couldNotConnect
|
||||
);
|
||||
cy.get(postgreSqlSelector.buttonSave)
|
||||
.verifyVisibleElement("have.text", postgreSqlText.buttonTextSave)
|
||||
.and("be.disabled");
|
||||
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
|
||||
"have.text",
|
||||
postgreSqlText.unableAcquireConnectionAlertText
|
||||
);
|
||||
|
||||
cy.get(dataSourceSelector.dropdownField(postgreSqlText.labelConnectionType))
|
||||
.should("be.visible")
|
||||
.click();
|
||||
cy.contains(
|
||||
`[id*="react-select-"]`,
|
||||
postgreSqlText.manualConnectionOption
|
||||
).click();
|
||||
|
||||
cy.get(
|
||||
dataSourceSelector.dropdownField(postgreSqlText.labelConnectionType)
|
||||
).should("be.visible");
|
||||
|
||||
const requiredFields = [
|
||||
postgreSqlText.labelHost,
|
||||
postgreSqlText.labelPort,
|
||||
postgreSqlText.labelUserName,
|
||||
postgreSqlText.labelPassword,
|
||||
];
|
||||
const sections = [
|
||||
postgreSqlText.labelHost,
|
||||
postgreSqlText.labelPort,
|
||||
postgreSqlText.labelDbName,
|
||||
postgreSqlText.labelUserName,
|
||||
postgreSqlText.labelPassword,
|
||||
postgreSqlText.labelConnectionOptions,
|
||||
];
|
||||
sections.forEach((section) => {
|
||||
if (section === postgreSqlText.labelConnectionOptions) {
|
||||
cy.get(dataSourceSelector.keyInputField(section, 0)).should(
|
||||
"be.visible"
|
||||
);
|
||||
cy.get(dataSourceSelector.valueInputField(section, 0)).should(
|
||||
"be.visible"
|
||||
);
|
||||
cy.get(dataSourceSelector.deleteButton(section, 0)).should(
|
||||
"be.visible"
|
||||
);
|
||||
cy.get(dataSourceSelector.addMoreButton(section)).should("be.visible");
|
||||
} else if (requiredFields.includes(section)) {
|
||||
cy.get(dataSourceSelector.labelFieldName(section)).verifyVisibleElement(
|
||||
"have.text",
|
||||
`${section}*`
|
||||
);
|
||||
cy.get(dataSourceSelector.textField(section)).should("be.visible");
|
||||
if (section === postgreSqlText.labelPassword) {
|
||||
cy.get(
|
||||
dataSourceSelector.button(postgreSqlText.editButtonText)
|
||||
).click();
|
||||
cy.verifyRequiredFieldValidation(section, "rgb(215, 45, 57)");
|
||||
} else {
|
||||
cy.get(dataSourceSelector.textField(section)).click();
|
||||
cy.get(commonSelectors.textField(section)).should(
|
||||
"have.css",
|
||||
"border-color",
|
||||
"rgba(0, 0, 0, 0)"
|
||||
);
|
||||
cy.get(dataSourceSelector.textField(section))
|
||||
.type("123")
|
||||
.clear()
|
||||
.blur();
|
||||
cy.verifyRequiredFieldValidation(section, "rgb(215, 45, 57)");
|
||||
}
|
||||
} else {
|
||||
cy.get(dataSourceSelector.labelFieldName(section)).verifyVisibleElement(
|
||||
"have.text",
|
||||
section
|
||||
);
|
||||
cy.get(dataSourceSelector.textField(section)).should("be.visible");
|
||||
}
|
||||
});
|
||||
cy.get(postgreSqlSelector.labelSsl).verifyVisibleElement(
|
||||
"have.text",
|
||||
postgreSqlText.labelSSL
|
||||
);
|
||||
cy.get(postgreSqlSelector.labelDbName).verifyVisibleElement(
|
||||
"have.text",
|
||||
postgreSqlText.labelDbName
|
||||
);
|
||||
cy.get(postgreSqlSelector.labelUserName).verifyVisibleElement(
|
||||
"have.text",
|
||||
postgreSqlText.labelUserName
|
||||
);
|
||||
cy.get(postgreSqlSelector.labelPassword).verifyVisibleElement(
|
||||
"have.text",
|
||||
postgreSqlText.labelPassword
|
||||
);
|
||||
cy.get(postgreSqlSelector.sslToggleInput).should("be.visible");
|
||||
cy.get(postgreSqlSelector.labelSSLCertificate).verifyVisibleElement(
|
||||
"have.text",
|
||||
postgreSqlText.sslCertificate
|
||||
|
|
@ -132,72 +249,85 @@ describe("Data sources", () => {
|
|||
"have.text",
|
||||
postgreSqlText.couldNotConnect
|
||||
);
|
||||
cy.get(postgreSqlSelector.buttonSave).verifyVisibleElement(
|
||||
cy.get(postgreSqlSelector.buttonSave)
|
||||
.verifyVisibleElement("have.text", postgreSqlText.buttonTextSave)
|
||||
.and("be.disabled");
|
||||
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
|
||||
"have.text",
|
||||
postgreSqlText.buttonTextSave
|
||||
"connect ECONNREFUSED 127.0.0.1:5432"
|
||||
);
|
||||
cy.get('[data-cy="connection-alert-text"]').should("be.visible");
|
||||
deleteDatasource(`cypress-${data.dataSourceName}-postgresql`);
|
||||
|
||||
cy.apiDeleteGDS(`cypress-${data.dataSourceName}-postgresql`);
|
||||
});
|
||||
|
||||
it.skip("Should verify the functionality of PostgreSQL connection form.", () => {
|
||||
selectAndAddDataSource(
|
||||
"databases",
|
||||
postgreSqlText.postgreSQL,
|
||||
data.dataSourceName
|
||||
it("Should verify the functionality of PostgreSQL connection form.", () => {
|
||||
cy.get(commonSelectors.globalDataSourceIcon).click();
|
||||
cy.apiCreateGDS(
|
||||
`${Cypress.env("server_host")}/api/data-sources`,
|
||||
`cypress-${data.dataSourceName}-manual-pgsql`,
|
||||
"postgresql",
|
||||
[
|
||||
{ key: "connection_type", value: "manual", encrypted: false },
|
||||
{ key: "host", value: `${Cypress.env("pg_host")}`, encrypted: false },
|
||||
{ key: "port", value: 5432, encrypted: false },
|
||||
{ key: "ssl_enabled", value: false, encrypted: false },
|
||||
{ key: "database", value: "postgres", encrypted: false },
|
||||
{ key: "ssl_certificate", value: "none", encrypted: false },
|
||||
{
|
||||
key: "username",
|
||||
value: `${Cypress.env("pg_user")}`,
|
||||
encrypted: false,
|
||||
},
|
||||
{
|
||||
key: "password",
|
||||
value: `${Cypress.env("pg_password")}`,
|
||||
encrypted: true,
|
||||
},
|
||||
{ key: "ca_cert", value: null, encrypted: true },
|
||||
{ key: "client_key", value: null, encrypted: true },
|
||||
{ key: "client_cert", value: null, encrypted: true },
|
||||
{ key: "root_cert", value: null, encrypted: true },
|
||||
{ key: "connection_string", value: null, encrypted: true },
|
||||
]
|
||||
);
|
||||
|
||||
fillDataSourceTextField(
|
||||
postgreSqlText.labelHost,
|
||||
postgreSqlText.placeholderEnterHost,
|
||||
Cypress.env("pg_host")
|
||||
);
|
||||
fillDataSourceTextField(
|
||||
postgreSqlText.labelPort,
|
||||
postgreSqlText.placeholderEnterPort,
|
||||
"5432"
|
||||
);
|
||||
cy.get('[data-cy="-toggle-input"]').then(($el) => {
|
||||
if ($el.is(":checked")) {
|
||||
cy.get('[data-cy="-toggle-input"]').uncheck();
|
||||
}
|
||||
});
|
||||
fillDataSourceTextField(
|
||||
postgreSqlText.labelDbName,
|
||||
postgreSqlText.placeholderNameOfDB,
|
||||
"postgres"
|
||||
);
|
||||
fillDataSourceTextField(
|
||||
postgreSqlText.labelUserName,
|
||||
postgreSqlText.placeholderEnterUserName,
|
||||
"postgres"
|
||||
);
|
||||
fillDataSourceTextField(
|
||||
postgreSqlText.labelPassword,
|
||||
"**************",
|
||||
Cypress.env("pg_password")
|
||||
);
|
||||
|
||||
cy.get(
|
||||
dataSourceSelector.dataSourceNameButton(
|
||||
`cypress-${data.dataSourceName}-manual-pgsql`
|
||||
)
|
||||
)
|
||||
.should("be.visible")
|
||||
.click();
|
||||
cy.get(postgreSqlSelector.buttonTestConnection).click();
|
||||
cy.get(postgreSqlSelector.textConnectionVerified, {
|
||||
timeout: 10000,
|
||||
}).should("have.text", postgreSqlText.labelConnectionVerified);
|
||||
cy.get(postgreSqlSelector.buttonSave).click();
|
||||
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
postgreSqlText.toastDSSaved
|
||||
cy.apiDeleteGDS(`cypress-${data.dataSourceName}-manual-pgsql`);
|
||||
cy.reload();
|
||||
cy.apiCreateGDS(
|
||||
`${Cypress.env("server_host")}/api/data-sources`,
|
||||
`cypress-${data.dataSourceName}-string-pgsql`,
|
||||
"postgresql",
|
||||
[
|
||||
{ key: "connection_type", value: "string", encrypted: false },
|
||||
{
|
||||
key: "connection_string",
|
||||
value: `${Cypress.env("pg_string")}`,
|
||||
encrypted: true,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
cy.get(commonSelectors.globalDataSourceIcon).click();
|
||||
cy.get(
|
||||
`[data-cy="cypress-${data.dataSourceName}-postgresql-button"]`
|
||||
).verifyVisibleElement(
|
||||
"have.text",
|
||||
`cypress-${data.dataSourceName}-postgresql`
|
||||
);
|
||||
|
||||
deleteDatasource(`cypress-${data.dataSourceName}-postgresql`);
|
||||
dataSourceSelector.dataSourceNameButton(
|
||||
`cypress-${data.dataSourceName}-string-pgsql`
|
||||
)
|
||||
)
|
||||
.should("be.visible")
|
||||
.click();
|
||||
cy.get(postgreSqlSelector.buttonTestConnection).click();
|
||||
cy.get(postgreSqlSelector.textConnectionVerified, {
|
||||
timeout: 10000,
|
||||
}).should("have.text", postgreSqlText.labelConnectionVerified);
|
||||
cy.apiDeleteGDS(`cypress-${data.dataSourceName}-string-pgsql`);
|
||||
});
|
||||
|
||||
it.skip("Should verify elements of the Query section.", () => {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { postgreSqlSelector } from "Selectors/postgreSql";
|
|||
import { postgreSqlText } from "Texts/postgreSql";
|
||||
import { redisText } from "Texts/redis";
|
||||
import { commonSelectors } from "Selectors/common";
|
||||
import { commonText } from "Texts/common";
|
||||
import { dataSourceSelector } from "Selectors/dataSource";
|
||||
|
||||
import {
|
||||
fillDataSourceTextField,
|
||||
|
|
@ -96,7 +96,7 @@ describe("Data source Redis", () => {
|
|||
"have.text",
|
||||
postgreSqlText.buttonTextSave
|
||||
);
|
||||
cy.get('[data-cy="connection-alert-text"]').should(
|
||||
cy.get(dataSourceSelector.connectionAlertText).should(
|
||||
"have.text",
|
||||
redisText.errorMaxRetries
|
||||
);
|
||||
|
|
@ -137,7 +137,7 @@ describe("Data source Redis", () => {
|
|||
);
|
||||
|
||||
cy.get(postgreSqlSelector.buttonTestConnection).click();
|
||||
cy.get('[data-cy="connection-alert-text"]').should(
|
||||
cy.get(dataSourceSelector.connectionAlertText).should(
|
||||
"have.text",
|
||||
redisText.errorInvalidUserOrPassword
|
||||
);
|
||||
|
|
@ -152,7 +152,7 @@ describe("Data source Redis", () => {
|
|||
"108299"
|
||||
);
|
||||
cy.get(postgreSqlSelector.buttonTestConnection).click();
|
||||
cy.get('[data-cy="connection-alert-text"]').should(
|
||||
cy.get(dataSourceSelector.connectionAlertText).should(
|
||||
"have.text",
|
||||
redisText.errorPort
|
||||
);
|
||||
|
|
@ -170,7 +170,7 @@ describe("Data source Redis", () => {
|
|||
);
|
||||
|
||||
cy.get(postgreSqlSelector.buttonTestConnection).click();
|
||||
cy.get('[data-cy="connection-alert-text"]').should(
|
||||
cy.get(dataSourceSelector.connectionAlertText).should(
|
||||
"have.text",
|
||||
redisText.errorInvalidUserOrPassword
|
||||
);
|
||||
|
|
@ -187,7 +187,7 @@ describe("Data source Redis", () => {
|
|||
"redis"
|
||||
);
|
||||
cy.get(postgreSqlSelector.buttonTestConnection).click();
|
||||
cy.get('[data-cy="connection-alert-text"]').should(
|
||||
cy.get(dataSourceSelector.connectionAlertText).should(
|
||||
"have.text",
|
||||
redisText.errorInvalidUserOrPassword
|
||||
);
|
||||
|
|
|
|||
|
|
@ -329,9 +329,9 @@ describe("Data source Rest API", () => {
|
|||
);
|
||||
cy.contains("Save").click();
|
||||
cy.verifyToastMessage(commonSelectors.toastMessage, "Data Source Saved");
|
||||
deleteDatasource(`cypress-${data.dataSourceName}-restapi`);
|
||||
cy.apiDeleteGDS(`cypress-${data.dataSourceName}-restapi`);
|
||||
});
|
||||
it("Should verify basic connection for Rest API", () => {
|
||||
it("Should verify connection response for all methods", () => {
|
||||
cy.apiCreateGDS(
|
||||
`${Cypress.env("server_host")}/api/data-sources`,
|
||||
`cypress-${data.dataSourceName}-restapi`,
|
||||
|
|
@ -367,88 +367,388 @@ describe("Data source Rest API", () => {
|
|||
);
|
||||
cy.reload();
|
||||
|
||||
cy.apiCreateApp(`${fake.companyName}-restAPI-App`);
|
||||
cy.apiCreateApp(`${fake.companyName}-restAPI-CURD-App`);
|
||||
cy.openApp();
|
||||
createAndRunRestAPIQuery(
|
||||
"get_restapi",
|
||||
`cypress-${data.dataSourceName}-restapi`,
|
||||
"GET",
|
||||
"/api/users"
|
||||
);
|
||||
createAndRunRestAPIQuery(
|
||||
"post_restapi",
|
||||
`cypress-${data.dataSourceName}-restapi`,
|
||||
"POST",
|
||||
"",
|
||||
[["Content-Type", "application/json"]],
|
||||
[],
|
||||
{
|
||||
price: 200,
|
||||
name: "Violin",
|
||||
},
|
||||
true,
|
||||
"/api/users"
|
||||
);
|
||||
createAndRunRestAPIQuery({
|
||||
queryName: "get_beeceptor_data",
|
||||
dsName: `cypress-${data.dataSourceName}-restapi`,
|
||||
method: "GET",
|
||||
urlSuffix: "/api/users",
|
||||
run: true,
|
||||
});
|
||||
createAndRunRestAPIQuery({
|
||||
queryName: "post_restapi",
|
||||
dsName: `cypress-${data.dataSourceName}-restapi`,
|
||||
method: "POST",
|
||||
headersList: [["Content-Type", "application/json"]],
|
||||
rawBody: '{"price": 200,"name": "Violin"}',
|
||||
urlSuffix: "/api/users",
|
||||
expectedResponseShape: { price: 200, name: "Violin", id: true },
|
||||
});
|
||||
cy.readFile("cypress/fixtures/restAPI/storedId.json").then(
|
||||
(postResponseID) => {
|
||||
const id1 = postResponseID.id;
|
||||
createAndRunRestAPIQuery(
|
||||
"put_restapi_id",
|
||||
`cypress-${data.dataSourceName}-restapi`,
|
||||
"PUT",
|
||||
"",
|
||||
[["Content-Type", "application/json"]],
|
||||
[],
|
||||
{
|
||||
price: 500,
|
||||
name: "Guitar",
|
||||
},
|
||||
true,
|
||||
`/api/users/${id1}`
|
||||
);
|
||||
}
|
||||
);
|
||||
cy.readFile("cypress/fixtures/restAPI/storedId.json").then(
|
||||
(putResponseID) => {
|
||||
const id2 = putResponseID.id;
|
||||
createAndRunRestAPIQuery(
|
||||
"patch_restapi_id",
|
||||
`cypress-${data.dataSourceName}-restapi`,
|
||||
"PATCH",
|
||||
"",
|
||||
[["Content-Type", "application/json"]],
|
||||
[],
|
||||
{ price: 999 },
|
||||
true,
|
||||
`/api/users/${id2}`
|
||||
);
|
||||
}
|
||||
);
|
||||
cy.readFile("cypress/fixtures/restAPI/storedId.json").then(
|
||||
(patchResponseID) => {
|
||||
const id3 = patchResponseID.id;
|
||||
createAndRunRestAPIQuery(
|
||||
"get_restapi_id",
|
||||
`cypress-${data.dataSourceName}-restapi`,
|
||||
"GET",
|
||||
"",
|
||||
[],
|
||||
[],
|
||||
true,
|
||||
`/api/users/${id3}`
|
||||
);
|
||||
|
||||
createAndRunRestAPIQuery(
|
||||
"delete_restapi_id",
|
||||
`cypress-${data.dataSourceName}-restapi`,
|
||||
"DELETE",
|
||||
"",
|
||||
[],
|
||||
[],
|
||||
true,
|
||||
`/api/users/${id3}`
|
||||
);
|
||||
createAndRunRestAPIQuery({
|
||||
queryName: "put_restapi_id",
|
||||
dsName: `cypress-${data.dataSourceName}-restapi`,
|
||||
method: "PUT",
|
||||
headersList: [["Content-Type", "application/json"]],
|
||||
rawBody: '{"price": 500,"name": "Guitar"}',
|
||||
urlSuffix: `/api/users/${id1}`,
|
||||
expectedResponseShape: { price: 500, name: "Guitar", id: id1 },
|
||||
});
|
||||
createAndRunRestAPIQuery({
|
||||
queryName: "patch_restapi_id",
|
||||
dsName: `cypress-${data.dataSourceName}-restapi`,
|
||||
method: "PATCH",
|
||||
headersList: [["Content-Type", "application/json"]],
|
||||
rawBody: '{"price": 999 }',
|
||||
urlSuffix: `/api/users/${id1}`,
|
||||
run: true,
|
||||
expectedResponseShape: { price: 999, id: id1 },
|
||||
});
|
||||
createAndRunRestAPIQuery({
|
||||
queryName: "get_restapi_id",
|
||||
dsName: `cypress-${data.dataSourceName}-restapi`,
|
||||
method: "GET",
|
||||
urlSuffix: `/api/users/${id1}`,
|
||||
run: true,
|
||||
expectedResponseShape: { price: 999, name: "Guitar", id: id1 },
|
||||
});
|
||||
createAndRunRestAPIQuery({
|
||||
queryName: "delete_restapi_id",
|
||||
dsName: `cypress-${data.dataSourceName}-restapi`,
|
||||
method: "DELETE",
|
||||
urlSuffix: `/api/users/${id1}`,
|
||||
run: true,
|
||||
expectedResponseShape: { success: true },
|
||||
});
|
||||
}
|
||||
);
|
||||
cy.apiDeleteApp(`${fake.companyName}-restAPI-CURD-App`);
|
||||
cy.apiDeleteGDS(`cypress-${data.dataSourceName}-restapi`);
|
||||
});
|
||||
it("Should verify response for basic authentication type connection", () => {
|
||||
cy.apiCreateGDS(
|
||||
`${Cypress.env("server_host")}/api/data-sources`,
|
||||
`cypress-${data.dataSourceName}-restapi`,
|
||||
"restapi",
|
||||
[
|
||||
{ key: "url", value: "https://httpbin.org" },
|
||||
{ key: "auth_type", value: "basic" },
|
||||
{ key: "grant_type", value: "authorization_code" },
|
||||
{ key: "add_token_to", value: "header" },
|
||||
{ key: "header_prefix", value: "Bearer " },
|
||||
{ key: "access_token_url", value: "" },
|
||||
{ key: "client_id", value: "" },
|
||||
{
|
||||
key: "client_secret",
|
||||
encrypted: true,
|
||||
credential_id: "b044a293-82b4-4381-84fd-d173c86a6a0c",
|
||||
},
|
||||
{ key: "audience", value: "" },
|
||||
{ key: "scopes", value: "read, write" },
|
||||
{ key: "username", value: "user", encrypted: false },
|
||||
{ key: "password", value: "pass", encrypted: true },
|
||||
{
|
||||
key: "bearer_token",
|
||||
encrypted: true,
|
||||
credential_id: "21caf3cb-dbde-43c7-9f42-77feffb63062",
|
||||
},
|
||||
{ key: "auth_url", value: "" },
|
||||
{ key: "client_auth", value: "header" },
|
||||
{ key: "headers", value: [["", ""]] },
|
||||
{ key: "custom_query_params", value: [["", ""]], encrypted: false },
|
||||
{ key: "custom_auth_params", value: [["", ""]] },
|
||||
{
|
||||
key: "access_token_custom_headers",
|
||||
value: [["", ""]],
|
||||
encrypted: false,
|
||||
},
|
||||
{ key: "multiple_auth_enabled", value: false, encrypted: false },
|
||||
{ key: "ssl_certificate", value: "none", encrypted: false },
|
||||
{ key: "retry_network_errors", value: true, encrypted: false },
|
||||
{ key: "url_parameters", value: [["", ""]], encrypted: false },
|
||||
{ key: "tokenData", encrypted: false },
|
||||
]
|
||||
);
|
||||
cy.reload();
|
||||
cy.intercept("GET", "/api/library_apps").as("appLibrary");
|
||||
cy.apiCreateApp(`${fake.companyName}-restAPI-Basic-App`);
|
||||
createAndRunRestAPIQuery({
|
||||
queryName: "get_basic_auth_valid",
|
||||
dsName: `cypress-${data.dataSourceName}-restapi`,
|
||||
method: "GET",
|
||||
urlSuffix: "/basic-auth/user/pass",
|
||||
expectedResponseShape: { authenticated: true, user: "user" },
|
||||
});
|
||||
createAndRunRestAPIQuery({
|
||||
queryName: "get_basic_auth_invalid",
|
||||
dsName: `cypress-${data.dataSourceName}-restapi`,
|
||||
method: "GET",
|
||||
urlSuffix: "/basic-auth/invaliduser/invalidpass",
|
||||
});
|
||||
cy.apiDeleteApp(`${fake.companyName}-restAPI-Basic-App`);
|
||||
cy.apiDeleteGDS(`cypress-${data.dataSourceName}-restapi`);
|
||||
});
|
||||
it("Should verify response for bearer authentication type connection", () => {
|
||||
cy.apiCreateGDS(
|
||||
`${Cypress.env("server_host")}/api/data-sources`,
|
||||
`cypress-${data.dataSourceName}-restapi`,
|
||||
"restapi",
|
||||
[
|
||||
{ key: "url", value: "https://httpbin.org" },
|
||||
{ key: "auth_type", value: "bearer" },
|
||||
{ key: "grant_type", value: "authorization_code" },
|
||||
{ key: "add_token_to", value: "header" },
|
||||
{ key: "header_prefix", value: "Bearer " },
|
||||
{ key: "access_token_url", value: "" },
|
||||
{ key: "client_id", value: "" },
|
||||
{
|
||||
key: "client_secret",
|
||||
encrypted: true,
|
||||
credential_id: "b044a293-82b4-4381-84fd-d173c86a6a0c",
|
||||
},
|
||||
{ key: "audience", value: "" },
|
||||
{ key: "scopes", value: "read, write" },
|
||||
{ key: "username", value: "", encrypted: false },
|
||||
{ key: "password", value: "", encrypted: true },
|
||||
{
|
||||
key: "bearer_token",
|
||||
value: "my-token-123",
|
||||
encrypted: true,
|
||||
},
|
||||
{ key: "auth_url", value: "" },
|
||||
{ key: "client_auth", value: "header" },
|
||||
{ key: "headers", value: [["", ""]] },
|
||||
{ key: "custom_query_params", value: [["", ""]], encrypted: false },
|
||||
{ key: "custom_auth_params", value: [["", ""]] },
|
||||
{
|
||||
key: "access_token_custom_headers",
|
||||
value: [["", ""]],
|
||||
encrypted: false,
|
||||
},
|
||||
{ key: "multiple_auth_enabled", value: false, encrypted: false },
|
||||
{ key: "ssl_certificate", value: "none", encrypted: false },
|
||||
{ key: "retry_network_errors", value: true, encrypted: false },
|
||||
{ key: "url_parameters", value: [["", ""]], encrypted: false },
|
||||
{ key: "tokenData", encrypted: false },
|
||||
]
|
||||
);
|
||||
cy.reload();
|
||||
cy.intercept("GET", "/api/library_apps").as("appLibrary");
|
||||
cy.apiCreateApp(`${fake.companyName}-restAPI-Bearer-App`);
|
||||
cy.openApp();
|
||||
createAndRunRestAPIQuery({
|
||||
queryName: "get_bearer_auth_valid",
|
||||
dsName: `cypress-${data.dataSourceName}-restapi`,
|
||||
method: "GET",
|
||||
urlSuffix: "/bearer",
|
||||
expectedResponseShape: { authenticated: true, token: "my-token-123" },
|
||||
});
|
||||
cy.apiDeleteApp(`${fake.companyName}-restAPI-Bearer-App`);
|
||||
cy.intercept("GET", "api/data_sources?**").as("datasource");
|
||||
cy.apiCreateGDS(
|
||||
`${Cypress.env("server_host")}/api/data-sources`,
|
||||
`cypress-${data.dataSourceName}-restapi-invalid`,
|
||||
"restapi",
|
||||
[
|
||||
{ key: "url", value: "https://httpbin.org" },
|
||||
{ key: "auth_type", value: "bearer" },
|
||||
{ key: "grant_type", value: "authorization_code" },
|
||||
{ key: "add_token_to", value: "header" },
|
||||
{ key: "header_prefix", value: "Bearer " },
|
||||
{ key: "access_token_url", value: "" },
|
||||
{ key: "client_id", value: "" },
|
||||
{
|
||||
key: "client_secret",
|
||||
encrypted: true,
|
||||
credential_id: "b044a293-82b4-4381-84fd-d173c86a6a0c",
|
||||
},
|
||||
{ key: "audience", value: "" },
|
||||
{ key: "scopes", value: "read, write" },
|
||||
{ key: "username", value: "", encrypted: false },
|
||||
{ key: "password", value: "", encrypted: true },
|
||||
{
|
||||
key: "bearer_token",
|
||||
value: "",
|
||||
encrypted: true,
|
||||
},
|
||||
{ key: "auth_url", value: "" },
|
||||
{ key: "client_auth", value: "header" },
|
||||
{ key: "headers", value: [["", ""]] },
|
||||
{ key: "custom_query_params", value: [["", ""]], encrypted: false },
|
||||
{ key: "custom_auth_params", value: [["", ""]] },
|
||||
{
|
||||
key: "access_token_custom_headers",
|
||||
value: [["", ""]],
|
||||
encrypted: false,
|
||||
},
|
||||
{ key: "multiple_auth_enabled", value: false, encrypted: false },
|
||||
{ key: "ssl_certificate", value: "none", encrypted: false },
|
||||
{ key: "retry_network_errors", value: true, encrypted: false },
|
||||
{ key: "url_parameters", value: [["", ""]], encrypted: false },
|
||||
{ key: "tokenData", encrypted: false },
|
||||
]
|
||||
);
|
||||
cy.apiCreateApp(`${fake.companyName}-restAPI-Bearer-invalid`);
|
||||
cy.openApp();
|
||||
createAndRunRestAPIQuery({
|
||||
queryName: "get_bearer_auth_invalid",
|
||||
dsName: `cypress-${data.dataSourceName}-restapi-invalid`,
|
||||
method: "GET",
|
||||
urlSuffix: "/bearer",
|
||||
});
|
||||
cy.apiDeleteApp(`${fake.companyName}-restAPI-Bearer-invalid`);
|
||||
cy.apiDeleteGDS(`cypress-${data.dataSourceName}-restapi`);
|
||||
});
|
||||
it.skip("Should verify response for authentication code grant type connection", () => {
|
||||
cy.apiCreateGDS(
|
||||
`${Cypress.env("server_host")}/api/data-sources`,
|
||||
`cypress-${data.dataSourceName}-restapi`,
|
||||
"restapi",
|
||||
[
|
||||
{
|
||||
key: "url",
|
||||
value: "https://dev-6lj2hoxdz5fg3m57.uk.auth0.com/api/v2/users",
|
||||
},
|
||||
{ key: "auth_type", value: "oauth2" },
|
||||
{ key: "grant_type", value: "client_credentials" },
|
||||
{ key: "add_token_to", value: "header" },
|
||||
{ key: "header_prefix", value: "Bearer " },
|
||||
{
|
||||
key: "access_token_url",
|
||||
value: "https://dev-6lj2hoxdz5fg3m57.uk.auth0.com/oauth/token",
|
||||
},
|
||||
{ key: "client_id", value: "JBDuuLU9vaSTP6Do7zYSkw0GvVgWhfyZ" },
|
||||
{
|
||||
key: "client_secret",
|
||||
encrypted: true,
|
||||
credential_id: "a6d26607-4d09-42a2-8bc0-e5c185c7c2f7",
|
||||
},
|
||||
{
|
||||
key: "audience",
|
||||
value: "https://dev-6lj2hoxdz5fg3m57.uk.auth0.com/api/v2/",
|
||||
},
|
||||
{ key: "scopes", value: "" },
|
||||
{ key: "username", value: "", encrypted: false },
|
||||
{
|
||||
key: "password",
|
||||
encrypted: true,
|
||||
credential_id: "4502a906-b512-447a-a128-39f67e9778d2",
|
||||
},
|
||||
{
|
||||
key: "bearer_token",
|
||||
encrypted: true,
|
||||
credential_id: "c94262c7-d2c5-4d7f-96f8-657689f2b1f0",
|
||||
},
|
||||
{ key: "auth_url", value: "" },
|
||||
{ key: "client_auth", value: "header" },
|
||||
{ key: "headers", value: [["", ""]] },
|
||||
{ key: "custom_query_params", value: [["", ""]], encrypted: false },
|
||||
{ key: "custom_auth_params", value: [["", ""]] },
|
||||
{
|
||||
key: "access_token_custom_headers",
|
||||
value: [["", ""]],
|
||||
encrypted: false,
|
||||
},
|
||||
{ key: "multiple_auth_enabled", value: false, encrypted: false },
|
||||
{ key: "ssl_certificate", value: "none", encrypted: false },
|
||||
{ key: "retry_network_errors", value: true, encrypted: false },
|
||||
]
|
||||
);
|
||||
cy.reload();
|
||||
cy.intercept("GET", "/api/library_apps").as("appLibrary");
|
||||
cy.apiCreateApp(`${fake.companyName}-client-Grant-RestAPI`);
|
||||
});
|
||||
it("Should verify response for content-type", () => {
|
||||
cy.apiCreateApp(`${fake.companyName}-restAPI-Content-App`);
|
||||
createAndRunRestAPIQuery({
|
||||
queryName: "post_json",
|
||||
dsName: "restapidefault",
|
||||
method: "POST",
|
||||
url: "https://jsonplaceholder.typicode.com/posts",
|
||||
headersList: [["Content-Type", "application/json"]],
|
||||
rawBody: '{"title": "foo","body": "bar","userId": 1}',
|
||||
run: true,
|
||||
urlSuffix: "",
|
||||
expectedResponseShape: { id: true, title: "foo", body: "bar", userId: 1 },
|
||||
});
|
||||
createAndRunRestAPIQuery({
|
||||
queryName: "post_raw_text",
|
||||
dsName: "restapidefault",
|
||||
method: "POST",
|
||||
url: "https://httpbin.org/post",
|
||||
headersList: [["Content-Type", "text/plain"]],
|
||||
rawBody: "This is plain text content",
|
||||
jsonBody: null,
|
||||
run: true,
|
||||
expectedResponseShape: { data: "This is plain text content" },
|
||||
});
|
||||
createAndRunRestAPIQuery({
|
||||
queryName: "post_form_urlencoded",
|
||||
dsName: "restapidefault",
|
||||
method: "POST",
|
||||
url: "https://httpbin.org/post",
|
||||
headersList: [["Content-Type", "application/x-www-form-urlencoded"]],
|
||||
bodyList: [
|
||||
["name", "Jane"],
|
||||
["age", "30"],
|
||||
],
|
||||
expectedResponseShape: {
|
||||
"form.name": "Jane",
|
||||
"form.age": "30",
|
||||
},
|
||||
});
|
||||
createAndRunRestAPIQuery({
|
||||
queryName: "post_xml_soap",
|
||||
dsName: "restapidefault",
|
||||
method: "POST",
|
||||
url: "http://webservices.oorsprong.org/websamples.countryinfo/CountryInfoService.wso?WSDL",
|
||||
headersList: [["Content-Type", "text/xml; charset=utf-8"]],
|
||||
rawBody: `<?xml version="1.0" encoding="utf-8"?>
|
||||
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<soap:Body>
|
||||
<ListOfContinentsByName xmlns="http://www.oorsprong.org/websamples.countryinfo">
|
||||
</ListOfContinentsByName>
|
||||
</soap:Body>
|
||||
</soap:Envelope>`,
|
||||
jsonBody: null,
|
||||
bodyList: [],
|
||||
cookiesList: [["session", "abc123"]],
|
||||
paramsList: [["lang", "en"]],
|
||||
run: true,
|
||||
shouldSucceed: true,
|
||||
expectedResponseShape: {},
|
||||
});
|
||||
// createAndRunRestAPIQuery({
|
||||
// queryName: "post_text_csv",
|
||||
// dsName: "restapidefault",
|
||||
// method: "POST",
|
||||
// url: `https://tejasvi.free.beeceptor.com/csv-upload`,
|
||||
// headersList: [["Content-Type", "text/csv"]],
|
||||
// rawBody:
|
||||
// "id,name,email\n1,Alice,alice@example.com\n2,Bob,bob@example.com",
|
||||
// expectedResponseShape: {
|
||||
// data: '{\n "status": "ok",\n "message": "File uploaded successfully",\n "body": id,name,email\n1,Alice,alice@example.com\n2,Bob,bob@example.com\n}',
|
||||
// },
|
||||
// });
|
||||
// const filename = "tooljet.png";
|
||||
|
||||
// createAndRunRestAPIQuery({
|
||||
// queryName: "upload_image",
|
||||
// dsName: "restapidefault",
|
||||
// method: "POST",
|
||||
// url: `https://tejasvi.free.beeceptor.commultipart-upload`,
|
||||
// headersList: [["Content-Type", "multipart/form-data"]],
|
||||
// bodyList: [
|
||||
// ["Image_File", "fixture:Image/tooljet.png"],
|
||||
// ["filename", filename],
|
||||
// ],
|
||||
// expectedResponseShape: {
|
||||
// filename: filename,
|
||||
// },
|
||||
// });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ describe("Data sources AWS S3", () => {
|
|||
"have.text",
|
||||
postgreSqlText.buttonTextSave
|
||||
);
|
||||
cy.get('[data-cy="connection-alert-text"]').should(
|
||||
cy.get(dataSourceSelector.connectionAlertText).should(
|
||||
"have.text",
|
||||
s3Text.alertRegionIsMissing
|
||||
);
|
||||
|
|
@ -144,7 +144,7 @@ describe("Data sources AWS S3", () => {
|
|||
);
|
||||
|
||||
cy.get(postgreSqlSelector.buttonTestConnection).click();
|
||||
cy.get('[data-cy="connection-alert-text"]').should(
|
||||
cy.get(dataSourceSelector.connectionAlertText).should(
|
||||
"have.text",
|
||||
s3Text.alertRegionIsMissing
|
||||
);
|
||||
|
|
@ -170,7 +170,7 @@ describe("Data sources AWS S3", () => {
|
|||
.click();
|
||||
|
||||
cy.get(postgreSqlSelector.buttonTestConnection).click();
|
||||
cy.get('[data-cy="connection-alert-text"]').should(
|
||||
cy.get(dataSourceSelector.connectionAlertText).should(
|
||||
"have.text",
|
||||
s3Text.alertInvalidUrl
|
||||
);
|
||||
|
|
@ -188,7 +188,7 @@ describe("Data sources AWS S3", () => {
|
|||
);
|
||||
|
||||
cy.get(postgreSqlSelector.buttonTestConnection).click();
|
||||
cy.get('[data-cy="connection-alert-text"]').should(
|
||||
cy.get(dataSourceSelector.connectionAlertText).should(
|
||||
"have.text",
|
||||
s3Text.accessKeyError
|
||||
);
|
||||
|
|
@ -207,7 +207,7 @@ describe("Data sources AWS S3", () => {
|
|||
|
||||
cy.get(postgreSqlSelector.buttonTestConnection).click();
|
||||
|
||||
cy.get('[data-cy="connection-alert-text"]').should(
|
||||
cy.get(dataSourceSelector.connectionAlertText).should(
|
||||
"have.text",
|
||||
s3Text.sinatureError
|
||||
);
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ describe("Data source SMTP", () => {
|
|||
"have.text",
|
||||
postgreSqlText.buttonTextSave
|
||||
);
|
||||
cy.get('[data-cy="connection-alert-text"]').should(
|
||||
cy.get(dataSourceSelector.connectionAlertText).should(
|
||||
"have.text",
|
||||
"Invalid credentials"
|
||||
);
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ describe("Data sources", () => {
|
|||
"have.text",
|
||||
postgreSqlText.buttonTextSave
|
||||
);
|
||||
cy.get('[data-cy="connection-alert-text"]').should(
|
||||
cy.get(dataSourceSelector.connectionAlertText).should(
|
||||
"have.text",
|
||||
"Invalid account. The specified value must be a valid subdomain string."
|
||||
);
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ describe("Data sources", () => {
|
|||
"have.text",
|
||||
postgreSqlText.buttonTextSave
|
||||
);
|
||||
cy.get('[data-cy="connection-alert-text"]').verifyVisibleElement(
|
||||
cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement(
|
||||
"have.text",
|
||||
"Failed to connect to localhost:1433 - Could not connect (sequence)"
|
||||
);
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ describe("Data sources", () => {
|
|||
"have.text",
|
||||
postgreSqlText.buttonTextSave
|
||||
);
|
||||
cy.get('[data-cy="connection-alert-text"]').should(
|
||||
cy.get(dataSourceSelector.connectionAlertText).should(
|
||||
"have.text",
|
||||
"Ensure that apiKey is set"
|
||||
);
|
||||
|
|
|
|||
|
|
@ -114,17 +114,30 @@ describe("App Version", () => {
|
|||
cy.wait(3000);
|
||||
|
||||
// cy.reload();
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible", { timeout: 10000 });
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible", {
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
// Preview and release verification
|
||||
cy.openInCurrentTab(commonWidgetSelector.previewButton);
|
||||
cy.url().should("include", "/home?version=v2");
|
||||
cy.openApp("", Cypress.env("workspaceId"), Cypress.env("appId"), commonWidgetSelector.draggableWidget("text1"));
|
||||
cy.openApp(
|
||||
"",
|
||||
Cypress.env("workspaceId"),
|
||||
Cypress.env("appId"),
|
||||
commonWidgetSelector.draggableWidget("text1")
|
||||
);
|
||||
releasedVersionAndVerify("v2");
|
||||
});
|
||||
|
||||
it("should verify version management with components and queries", () => {
|
||||
// Initial setup with component and datasource
|
||||
cy.apiCreateGDS(
|
||||
`${Cypress.env("server_host")}/api/data-sources`,
|
||||
data.datasourceName,
|
||||
"restapi",
|
||||
[{ key: "url", value: "https://jsonplaceholder.typicode.com/users" }]
|
||||
);
|
||||
cy.apiAddComponentToApp(
|
||||
data.appName,
|
||||
"text1",
|
||||
|
|
@ -134,19 +147,15 @@ describe("App Version", () => {
|
|||
);
|
||||
cy.waitForAutoSave();
|
||||
|
||||
cy.apiCreateGDS(
|
||||
`${Cypress.env("server_host")}/api/data-sources`,
|
||||
data.datasourceName,
|
||||
"restapi",
|
||||
[{ key: "url", value: "https://jsonplaceholder.typicode.com/users" }]
|
||||
);
|
||||
createRestAPIQuery(data.query1, data.datasourceName, "", "", "/1", true);
|
||||
|
||||
// Version v2 creation and verification
|
||||
navigateToCreateNewVersionModal("v1");
|
||||
createNewVersion(["v2"], "v1");
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1"))
|
||||
.verifyVisibleElement("have.text", "Leanne Graham");
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).verifyVisibleElement(
|
||||
"have.text",
|
||||
"Leanne Graham"
|
||||
);
|
||||
cy.get(`[data-cy="list-query-${data.query1}"]`).should("be.visible");
|
||||
|
||||
// Modify v2 with new components and queries
|
||||
|
|
@ -170,67 +179,79 @@ describe("App Version", () => {
|
|||
create: { version: "v3", from: "v2" },
|
||||
verify: {
|
||||
component: { selector: "textInput", value: "Ervin Howell" },
|
||||
query: data.query2
|
||||
}
|
||||
query: data.query2,
|
||||
},
|
||||
},
|
||||
{
|
||||
create: { version: "v4", from: "v1" },
|
||||
verify: {
|
||||
component: { selector: "text1", text: "Leanne Graham" },
|
||||
query: data.query1
|
||||
}
|
||||
query: data.query1,
|
||||
},
|
||||
},
|
||||
{
|
||||
create: { version: "v5", from: "v3" },
|
||||
verify: {
|
||||
component: { selector: "textInput", value: "Ervin Howell" },
|
||||
query: data.query2
|
||||
}
|
||||
}
|
||||
query: data.query2,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
versionChecks.forEach(check => {
|
||||
versionChecks.forEach((check) => {
|
||||
navigateToCreateNewVersionModal(check.create.from);
|
||||
createNewVersion([check.create.version], check.create.from);
|
||||
|
||||
if (check.verify.component.value) {
|
||||
cy.get(commonWidgetSelector.draggableWidget(check.verify.component.selector))
|
||||
.verifyVisibleElement("have.value", check.verify.component.value);
|
||||
cy.get(
|
||||
commonWidgetSelector.draggableWidget(check.verify.component.selector)
|
||||
).verifyVisibleElement("have.value", check.verify.component.value);
|
||||
} else {
|
||||
cy.get(commonWidgetSelector.draggableWidget(check.verify.component.selector))
|
||||
.verifyVisibleElement("have.text", check.verify.component.text);
|
||||
cy.get(
|
||||
commonWidgetSelector.draggableWidget(check.verify.component.selector)
|
||||
).verifyVisibleElement("have.text", check.verify.component.text);
|
||||
}
|
||||
cy.get(`[data-cy="list-query-${check.verify.query}"]`).should("be.visible");
|
||||
cy.get(`[data-cy="list-query-${check.verify.query}"]`).should(
|
||||
"be.visible"
|
||||
);
|
||||
});
|
||||
|
||||
// Release and version state verification
|
||||
releasedVersionAndVerify("v5");
|
||||
cy.get(appVersionSelectors.currentVersionField("v5"))
|
||||
.should("have.class", "color-light-green");
|
||||
cy.get(appVersionSelectors.currentVersionField("v5")).should(
|
||||
"have.class",
|
||||
"color-light-green"
|
||||
);
|
||||
|
||||
// Version switching and component verification
|
||||
cy.get(appVersionSelectors.currentVersionField("v5")).click();
|
||||
cy.contains(`[id*="react-select-"]`, "v4").click();
|
||||
cy.get(appVersionSelectors.currentVersionField("v4"))
|
||||
.should("not.have.class", "color-light-green");
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1"))
|
||||
.verifyVisibleElement("have.text", "Leanne Graham");
|
||||
cy.get(appVersionSelectors.currentVersionField("v4")).should(
|
||||
"not.have.class",
|
||||
"color-light-green"
|
||||
);
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).verifyVisibleElement(
|
||||
"have.text",
|
||||
"Leanne Graham"
|
||||
);
|
||||
cy.get(`[data-cy="list-query-${data.query1}"]`).should("be.visible");
|
||||
|
||||
// Preview and version switching verification
|
||||
cy.openInCurrentTab(commonWidgetSelector.previewButton);
|
||||
cy.url().should("include", "/home?version=v4");
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1"))
|
||||
.verifyVisibleElement("have.text", "Leanne Graham");
|
||||
cy.get(commonWidgetSelector.draggableWidget("text1")).verifyVisibleElement(
|
||||
"have.text",
|
||||
"Leanne Graham"
|
||||
);
|
||||
|
||||
cy.get('[data-cy="preview-settings"]').click();
|
||||
switchVersionAndVerify("v4", "v5");
|
||||
|
||||
cy.get(commonWidgetSelector.draggableWidget("textInput"))
|
||||
.verifyVisibleElement("have.value", "Ervin Howell");
|
||||
cy.get(
|
||||
commonWidgetSelector.draggableWidget("textInput")
|
||||
).verifyVisibleElement("have.value", "Ervin Howell");
|
||||
//url validation should be added after bug fix
|
||||
|
||||
// cy.url().should("include", "/home?version=v5");
|
||||
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ export const resolveHost = () => {
|
|||
const baseUrl = Cypress.config("baseUrl");
|
||||
|
||||
const urlMapping = {
|
||||
"http://localhost:8082": "http://localhost:8082",
|
||||
"http://localhost:3000": "http://localhost:3000",
|
||||
"http://localhost:3000/apps": "http://localhost:3000/apps",
|
||||
"http://localhost:4001": "http://localhost:3000",
|
||||
"http://localhost:4001/apps": "http://localhost:3000/apps",
|
||||
|
|
|
|||
|
|
@ -255,7 +255,7 @@ export const createRestAPIQuery = (
|
|||
}).then((response) => {
|
||||
const editingVersionId = response.body.editing_version.id;
|
||||
|
||||
const data_source_id = Cypress.env(kind);
|
||||
const data_source_id = Cypress.env(`${dsName}`);
|
||||
|
||||
const requestBody = {
|
||||
app_id: Cypress.env("appId"),
|
||||
|
|
|
|||
|
|
@ -1,19 +1,31 @@
|
|||
export const createAndRunRestAPIQuery = (
|
||||
export const createAndRunRestAPIQuery = ({
|
||||
queryName,
|
||||
dsName,
|
||||
method = "GET",
|
||||
url = "",
|
||||
urlSuffix = "",
|
||||
headersList = [],
|
||||
bodyList = [],
|
||||
jsonBody = null,
|
||||
rawBody = null,
|
||||
cookiesList = [],
|
||||
paramsList = [],
|
||||
run = true,
|
||||
urlSuffix = ""
|
||||
) => {
|
||||
expectedResponseShape = {},
|
||||
authType = "",
|
||||
authToken = "",
|
||||
}) => {
|
||||
cy.getCookie("tj_auth_token").then((cookie) => {
|
||||
const headers = {
|
||||
"Tj-Workspace-Id": Cypress.env("workspaceId"),
|
||||
Cookie: `tj_auth_token=${cookie.value}`,
|
||||
};
|
||||
// if (authType === "bearer" || authType === "oauth2") {
|
||||
// headers["Authorization"] = `Bearer ${authToken}`;
|
||||
// } else if (authType === "basic") {
|
||||
// headers["Authorization"] = `Basic ${btoa(authToken)}`;
|
||||
// }
|
||||
|
||||
cy.request({
|
||||
method: "GET",
|
||||
url: `${Cypress.env("server_host")}/api/apps/${Cypress.env("appId")}`,
|
||||
|
|
@ -27,84 +39,108 @@ export const createAndRunRestAPIQuery = (
|
|||
url: `${Cypress.env("server_host")}/api/data-sources/${Cypress.env("workspaceId")}/environments/${currentEnvironmentId}/versions/${editingVersionId}`,
|
||||
headers,
|
||||
}).then((dsResponse) => {
|
||||
expect(dsResponse.status).to.eq(200);
|
||||
|
||||
const dataSource = dsResponse.body.data_sources.find(
|
||||
(ds) => ds.name === dsName
|
||||
);
|
||||
|
||||
if (!dataSource) {
|
||||
throw new Error(`Data source '${dsName}' not found.`);
|
||||
}
|
||||
|
||||
const data_source_id = dataSource.id;
|
||||
const useJsonBody =
|
||||
["POST", "PATCH", "PUT"].includes(method.toUpperCase()) &&
|
||||
jsonBody !== null;
|
||||
const useJson = jsonBody !== null;
|
||||
const useRaw = rawBody !== null;
|
||||
const useForm = bodyList?.length && !useJson && !useRaw;
|
||||
|
||||
const queryOptions = {
|
||||
method: method.toLowerCase(),
|
||||
url: url + urlSuffix,
|
||||
url_params: [["", ""]],
|
||||
url_params: paramsList.length ? paramsList : [["", ""]],
|
||||
headers: headersList.length ? headersList : [["", ""]],
|
||||
body: !useJsonBody && bodyList.length ? bodyList : [["", ""]],
|
||||
json_body: useJsonBody ? jsonBody : null,
|
||||
body_toggle: useJsonBody,
|
||||
cookies: cookiesList.length ? cookiesList : [["", ""]],
|
||||
body: useForm ? bodyList : [["", ""]],
|
||||
json_body: useJson ? jsonBody : null,
|
||||
raw_body: useRaw ? rawBody : "",
|
||||
body_toggle: useJson || useRaw,
|
||||
runOnPageLoad: run,
|
||||
transformationLanguage: "javascript",
|
||||
enableTransformation: false,
|
||||
};
|
||||
|
||||
const requestBody = {
|
||||
app_id: Cypress.env("appId"),
|
||||
app_version_id: editingVersionId,
|
||||
name: queryName,
|
||||
kind: "restapi",
|
||||
options: queryOptions,
|
||||
data_source_id,
|
||||
data_source_id: dataSource.id,
|
||||
plugin_id: null,
|
||||
};
|
||||
|
||||
cy.request({
|
||||
method: "POST",
|
||||
url: `${Cypress.env("server_host")}/api/data-queries/data-sources/${data_source_id}/versions/${editingVersionId}`,
|
||||
url: `${Cypress.env("server_host")}/api/data-queries/data-sources/${dataSource.id}/versions/${editingVersionId}`,
|
||||
headers,
|
||||
body: requestBody,
|
||||
}).then((createResponse) => {
|
||||
expect(createResponse.status).to.equal(201);
|
||||
expect(createResponse.status).to.eq(201);
|
||||
const queryId = createResponse.body.id;
|
||||
cy.log("Query created successfully:", queryId);
|
||||
|
||||
const createdOptions = createResponse.body.options;
|
||||
expect(createdOptions.method).to.equal(queryOptions.method);
|
||||
expect(createdOptions.url).to.equal(queryOptions.url);
|
||||
expect(createdOptions.headers).to.deep.equal(queryOptions.headers);
|
||||
|
||||
if (useJsonBody) {
|
||||
expect(createdOptions.json_body).to.deep.equal(
|
||||
queryOptions.json_body
|
||||
);
|
||||
expect(createdOptions.body_toggle).to.equal(true);
|
||||
} else {
|
||||
expect(createdOptions.body).to.deep.equal(queryOptions.body);
|
||||
expect(createdOptions.body_toggle).to.equal(false);
|
||||
}
|
||||
|
||||
expect(createdOptions.runOnPageLoad).to.equal(run);
|
||||
cy.log("Metadata verified successfully");
|
||||
if (run) {
|
||||
cy.request({
|
||||
method: "POST",
|
||||
url: `${Cypress.env("server_host")}/api/data-queries/${queryId}/run`,
|
||||
headers,
|
||||
failOnStatusCode: false,
|
||||
}).then((runResponse) => {
|
||||
expect([200, 201]).to.include(runResponse.status);
|
||||
cy.log("Query executed successfully:", runResponse.body);
|
||||
if (runResponse.body?.data.id) {
|
||||
cy.writeFile("cypress/fixtures/restAPI/storedId.json", {
|
||||
id: runResponse.body.data.id,
|
||||
});
|
||||
cy.log("Stored ID:", runResponse.body.data.id);
|
||||
const responseData = runResponse.body?.data;
|
||||
const requestHeaders =
|
||||
runResponse.body?.metadata?.request?.headers || {};
|
||||
|
||||
if (runResponse.body.status === "ok") {
|
||||
expect([200, 201]).to.include(runResponse.status);
|
||||
cy.log("Response:", responseData);
|
||||
if (
|
||||
expectedResponseShape &&
|
||||
typeof expectedResponseShape === "object"
|
||||
) {
|
||||
Object.entries(expectedResponseShape).forEach(
|
||||
([path, expected]) => {
|
||||
const value = path
|
||||
.split(".")
|
||||
.reduce((obj, key) => obj?.[key], responseData);
|
||||
|
||||
if (expected === true) {
|
||||
expect(value).to.not.be.undefined;
|
||||
} else {
|
||||
expect(value).to.eq(expected);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const expectedContentType = headersList.find(
|
||||
([key]) => key.toLowerCase() === "content-type"
|
||||
)?.[1];
|
||||
if (expectedContentType && requestHeaders["content-type"]) {
|
||||
expect(requestHeaders["content-type"]).to.include(
|
||||
expectedContentType
|
||||
);
|
||||
}
|
||||
if (Array.isArray(responseData)) {
|
||||
responseData.forEach((item) => {
|
||||
expect(item).to.have.any.keys("id", "name", "price");
|
||||
});
|
||||
}
|
||||
if (responseData?.id) {
|
||||
cy.writeFile("cypress/fixtures/restAPI/storedId.json", {
|
||||
id: responseData.id,
|
||||
});
|
||||
}
|
||||
} else if (runResponse.body.status === "failed") {
|
||||
expect(runResponse.body.message).to.eq(
|
||||
"Query could not be completed"
|
||||
);
|
||||
const statusCode =
|
||||
runResponse.body?.metadata?.response?.statusCode;
|
||||
expect([400, 401, 403, 404, 500]).to.include(statusCode);
|
||||
cy.log(
|
||||
"Failure validated as expected with status:",
|
||||
statusCode
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
3.13.0
|
||||
3.14.0
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 777446d71e78e5941d34353606a12d982820438f
|
||||
Subproject commit aa3c4f603f549337fc88a772a6a31e18eaf38701
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
import React from 'react';
|
||||
import cx from 'classnames';
|
||||
import { pluginsService, marketplaceService } from '@/_services';
|
||||
import { pluginsService, marketplaceService, globalDatasourceService } from '@/_services';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import Spinner from '@/_ui/Spinner';
|
||||
import { capitalizeFirstLetter, useTagsByPluginId } from './utils';
|
||||
import { ConfirmDialog } from '@/_components';
|
||||
import Icon from '@/_ui/Icon/SolidIcons';
|
||||
import config from 'config';
|
||||
import Modal from '@/HomePage/Modal';
|
||||
|
||||
export const InstalledPlugins = () => {
|
||||
const [allPlugins, setAllPlugins] = React.useState([]);
|
||||
|
|
@ -81,6 +82,7 @@ const InstalledPluginCard = ({ plugin, marketplacePlugin, fetchPlugins, isDevMod
|
|||
const [updating, setUpdating] = React.useState(false);
|
||||
const [isDeleteModalVisible, setDeleteModalVisibility] = React.useState(false);
|
||||
const [isDeletingPlugin, setDeletingPlugin] = React.useState(false);
|
||||
const [showDependentQueriesInfo, setShowDependentQueriesInfo] = React.useState(false);
|
||||
|
||||
const darkMode = localStorage.getItem('darkMode') === 'true';
|
||||
const { id, name, pluginId } = plugin;
|
||||
|
|
@ -140,6 +142,21 @@ const InstalledPluginCard = ({ plugin, marketplacePlugin, fetchPlugins, isDevMod
|
|||
toast.success(`${capitalizeFirstLetter(name)} reloaded`);
|
||||
};
|
||||
|
||||
const getQueriesLinkedToMarketplacePlugin = (plugin) => {
|
||||
globalDatasourceService
|
||||
.getQueriesLinkedToMarketplacePlugin(plugin.id)
|
||||
.then((data) => {
|
||||
if (data?.dependent_queries) {
|
||||
setShowDependentQueriesInfo(true);
|
||||
} else {
|
||||
setDeleteModalVisibility(true);
|
||||
}
|
||||
})
|
||||
.catch(({ error }) => {
|
||||
toast.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
const pluginDeleteMessage = (
|
||||
<>
|
||||
Deleting <strong>{capitalizeFirstLetter(name)}</strong> plugin will result in the permanent removal of all
|
||||
|
|
@ -150,6 +167,15 @@ const InstalledPluginCard = ({ plugin, marketplacePlugin, fetchPlugins, isDevMod
|
|||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
title="Dependent queries found!"
|
||||
show={showDependentQueriesInfo}
|
||||
closeModal={() => setShowDependentQueriesInfo(false)}
|
||||
>
|
||||
<div className="mt-3 mb-3">
|
||||
Cannot delete the <b>{plugin?.name}</b> plugin as it is used in the apps
|
||||
</div>
|
||||
</Modal>
|
||||
<ConfirmDialog
|
||||
title={'Delete plugin'}
|
||||
show={isDeleteModalVisible}
|
||||
|
|
@ -238,7 +264,7 @@ const InstalledPluginCard = ({ plugin, marketplacePlugin, fetchPlugins, isDevMod
|
|||
<div className="col-auto">
|
||||
<div
|
||||
className={cx('cursor-pointer link-primary', { disabled: updating })}
|
||||
onClick={() => setDeleteModalVisibility(true)}
|
||||
onClick={() => getQueriesLinkedToMarketplacePlugin(plugin)}
|
||||
>
|
||||
Remove
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,11 @@ export const MarketplaceCard = ({ id, name, repo, description, version, isInstal
|
|||
}, [isInstalled]);
|
||||
|
||||
const installPlugin = async () => {
|
||||
if (installed) {
|
||||
toast.error(`${capitalizeFirstLetter(name)} is already installed.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const body = {
|
||||
id,
|
||||
name,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useLayoutEffect, useRef } from 'react';
|
||||
import { openapiService } from '@/_services';
|
||||
import Select from '@/_ui/Select';
|
||||
import { queryManagerSelectComponentStyle } from '@/_ui/Select/styles';
|
||||
|
|
@ -110,7 +110,7 @@ const ApiEndpointInput = (props) => {
|
|||
if (isEmpty(paths)) return [];
|
||||
|
||||
const pathGroups = Object.keys(paths).reduce((acc, path) => {
|
||||
const operations = Object.keys(paths[path]);
|
||||
const operations = Object.keys(paths[path]).filter((op) => Object.keys(operationColorMapping).includes(op));
|
||||
const category = path.split('/')[2];
|
||||
operations.forEach((operation) => categorizeOperations(operation, path, acc, category));
|
||||
return acc;
|
||||
|
|
@ -135,7 +135,7 @@ const ApiEndpointInput = (props) => {
|
|||
{loadingSpec && (
|
||||
<div className="p-3">
|
||||
<div className="spinner-border spinner-border-sm text-azure mx-2" role="status"></div>
|
||||
{props.t('stripe', 'Please wait while we load the OpenAPI specification.')}
|
||||
<span>Please wait while we load the OpenAPI specification.</span>
|
||||
</div>
|
||||
)}
|
||||
{options && !loadingSpec && (
|
||||
|
|
@ -227,57 +227,64 @@ const RenderParameterFields = ({ parameters, type, label, options, changeParam,
|
|||
}
|
||||
|
||||
const paramLabelWithDescription = (param) => {
|
||||
const label = type === 'request' ? param : param.name;
|
||||
const description = type === 'request' ? parameters[param]?.description : param.description;
|
||||
|
||||
return (
|
||||
<ToolTip message={type === 'request' ? DOMPurify.sanitize(parameters[param].description) : param.description}>
|
||||
<div className="cursor-help">
|
||||
<input
|
||||
type="text"
|
||||
value={type === 'request' ? param : param.name}
|
||||
className="form-control form-control-underline"
|
||||
placeholder="key"
|
||||
disabled
|
||||
/>
|
||||
<ToolTip message={DOMPurify.sanitize(description)}>
|
||||
<div className="cursor-help d-flex align-items-center">
|
||||
<AutoWidthText value={label} className="form-control form-control-underline" />
|
||||
</div>
|
||||
</ToolTip>
|
||||
);
|
||||
};
|
||||
|
||||
const paramLabelWithoutDescription = (param) => {
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
value={type === 'request' ? param : param.name}
|
||||
className="form-control"
|
||||
placeholder="key"
|
||||
disabled
|
||||
/>
|
||||
);
|
||||
};
|
||||
const label = type === 'request' ? param : param.name;
|
||||
|
||||
const paramType = (param) => {
|
||||
return (
|
||||
<div className="p-2 text-muted">
|
||||
{type === 'query' &&
|
||||
param?.schema?.anyOf &&
|
||||
param?.schema?.anyOf.map((type, i) =>
|
||||
i < param.schema?.anyOf.length - 1
|
||||
? type.type.substring(0, 3).toUpperCase() + '|'
|
||||
: type.type.substring(0, 3).toUpperCase()
|
||||
)}
|
||||
{(type === 'path' || (type === 'query' && !param?.schema?.anyOf)) &&
|
||||
param?.schema?.type?.substring(0, 3).toUpperCase()}
|
||||
{type === 'request' && parameters[param].type?.substring(0, 3).toUpperCase()}
|
||||
<div className="d-flex align-items-center" style={{ gap: '4px' }}>
|
||||
<AutoWidthText value={label} className="form-control" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const paramType = (param) => {
|
||||
let paramTypeValue;
|
||||
|
||||
if (type === 'query') {
|
||||
if (param?.schema?.anyOf) {
|
||||
return (
|
||||
<div className="p-2 text-muted">
|
||||
{param.schema.anyOf.map((typeObj, i) =>
|
||||
i < param.schema.anyOf.length - 1
|
||||
? (typeObj.type || '').toString().substring(0, 3).toUpperCase() + '|'
|
||||
: (typeObj.type || '').toString().substring(0, 3).toUpperCase()
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
paramTypeValue = param?.schema?.type;
|
||||
} else if (type === 'path') {
|
||||
paramTypeValue = param?.schema?.type;
|
||||
} else if (type === 'request') {
|
||||
paramTypeValue = parameters[param]?.type;
|
||||
}
|
||||
|
||||
const displayType = Array.isArray(paramTypeValue) ? paramTypeValue[0] : paramTypeValue;
|
||||
|
||||
return <div className="p-2 text-muted">{displayType?.toString().substring(0, 3).toUpperCase() || ''}</div>;
|
||||
};
|
||||
|
||||
const paramDetails = (param) => {
|
||||
return (
|
||||
<div className="col-auto d-flex field field-width-179 align-items-center">
|
||||
{(type === 'request' && parameters[param].description) || param?.description
|
||||
? paramLabelWithDescription(param)
|
||||
: paramLabelWithoutDescription(param)}
|
||||
{param.required && <span className="text-danger fw-bold">*</span>}
|
||||
<div className="col-auto d-flex field field-width-179 align-items-center justify-content-between">
|
||||
<div className="d-inline-flex align-items-center gap-3">
|
||||
{(type === 'request' && parameters[param].description) || param?.description
|
||||
? paramLabelWithDescription(param)
|
||||
: paramLabelWithoutDescription(param)}
|
||||
{param.required && <span className="text-danger fw-bold">*</span>}
|
||||
</div>
|
||||
{paramType(param)}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -359,3 +366,34 @@ RenderParameterFields.propTypes = {
|
|||
removeParam: PropTypes.func,
|
||||
darkMode: PropTypes.bool,
|
||||
};
|
||||
|
||||
const AutoWidthText = ({ value, className }) => {
|
||||
const spanRef = useRef(null);
|
||||
const [width, setWidth] = useState(0);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (spanRef.current) {
|
||||
setWidth(spanRef.current.offsetWidth);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div className={className} style={{ display: 'inline-block', width: width ? `${width}px` : 'auto' }}>
|
||||
<span
|
||||
ref={spanRef}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
visibility: 'hidden',
|
||||
whiteSpace: 'pre',
|
||||
fontSize: '12px',
|
||||
fontFamily: 'inherit',
|
||||
fontWeight: 400,
|
||||
lineHeight: '20px',
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
{value}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -245,7 +245,7 @@ const DynamicForm = ({
|
|||
encrypted,
|
||||
placeholders = {},
|
||||
editorType = 'basic',
|
||||
specUrl = '',
|
||||
spec_url = '',
|
||||
disabled = false,
|
||||
buttonText,
|
||||
text,
|
||||
|
|
@ -486,7 +486,7 @@ const DynamicForm = ({
|
|||
};
|
||||
case 'react-component-api-endpoint':
|
||||
return {
|
||||
specUrl: specUrl,
|
||||
specUrl: spec_url,
|
||||
optionsChanged,
|
||||
options,
|
||||
darkMode,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { datasourceService } from '@/_services';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
|
@ -15,8 +15,16 @@ const Slack = ({
|
|||
isDisabled,
|
||||
}) => {
|
||||
const [authStatus, setAuthStatus] = useState(null);
|
||||
const whiteLabelText = retrieveWhiteLabelText();
|
||||
const [whiteLabelText, setWhiteLabelText] = useState('');
|
||||
const plugin_id = selectedDataSource?.plugin?.id;
|
||||
const { t } = useTranslation();
|
||||
useEffect(() => {
|
||||
async function fetchLabel() {
|
||||
const text = await retrieveWhiteLabelText();
|
||||
setWhiteLabelText(text);
|
||||
}
|
||||
fetchLabel();
|
||||
}, []);
|
||||
|
||||
function authGoogle() {
|
||||
const provider = 'slack';
|
||||
|
|
@ -29,7 +37,7 @@ const Slack = ({
|
|||
}
|
||||
|
||||
datasourceService
|
||||
.fetchOauth2BaseUrl(provider)
|
||||
.fetchOauth2BaseUrl(provider, plugin_id, {})
|
||||
.then((data) => {
|
||||
const authUrl = `${data.url}&scope=${scope}&access_type=offline&prompt=select_account`;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Input from '@/_ui/Input';
|
||||
|
|
@ -18,17 +18,26 @@ const Zendesk = ({
|
|||
isDisabled,
|
||||
optionsChanged,
|
||||
}) => {
|
||||
const [whiteLabelText, setWhiteLabelText] = useState('');
|
||||
const [authStatus, setAuthStatus] = useState(null);
|
||||
const whiteLabelText = retrieveWhiteLabelText();
|
||||
useEffect(() => {
|
||||
async function fetchLabel() {
|
||||
const text = await retrieveWhiteLabelText();
|
||||
setWhiteLabelText(text);
|
||||
}
|
||||
fetchLabel();
|
||||
}, []);
|
||||
|
||||
function authZendesk() {
|
||||
const provider = 'zendesk';
|
||||
setAuthStatus('waiting_for_url');
|
||||
|
||||
const scope = options?.access_type?.value === 'read' ? 'read' : 'read%20write';
|
||||
const subDomain = options?.subdomain?.value;
|
||||
const client_id = options?.client_id?.value;
|
||||
|
||||
try {
|
||||
const authUrl = `https://${options?.subdomain?.value}.zendesk.com/oauth/authorizations/new?response_type=code&client_id=${options?.client_id?.value}&redirect_uri=${window.location.origin}/oauth2/authorize&scope=${scope}`;
|
||||
const authUrl = `https://${subDomain}.zendesk.com/oauth/authorizations/new?response_type=code&client_id=${client_id}&redirect_uri=${window.location.origin}/oauth2/authorize&scope=${scope}`;
|
||||
localStorage.setItem('sourceWaitingForOAuth', 'newSource');
|
||||
localStorage.setItem('currentAppEnvironmentIdForOauth', currentAppEnvironmentId);
|
||||
optionchanged('provider', provider).then(() => {
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ function setOauth2Token(dataSourceId, body, current_organization_id) {
|
|||
function fetchOauth2BaseUrl(provider, plugin_id = null, source_options = {}) {
|
||||
const payload = { provider, ...(plugin_id && { plugin_id }), ...(source_options && { source_options }) };
|
||||
const requestOptions = {
|
||||
method: 'GET',
|
||||
method: 'POST',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(payload),
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ export const globalDatasourceService = {
|
|||
convertToGlobal,
|
||||
getDataSourceByEnvironmentId,
|
||||
getForApp,
|
||||
getQueriesLinkedToDatasource,
|
||||
getQueriesLinkedToMarketplacePlugin,
|
||||
};
|
||||
|
||||
function getForApp(organizationId, appVersionId, environmentId) {
|
||||
|
|
@ -68,3 +70,15 @@ function getDataSourceByEnvironmentId(dataSourceId, environmentId) {
|
|||
handleResponse
|
||||
);
|
||||
}
|
||||
|
||||
function getQueriesLinkedToMarketplacePlugin(pluginId) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||
return fetch(`${config.apiUrl}/data-sources/dependent-queries/marketplace-plugin/${pluginId}`, requestOptions).then(
|
||||
handleResponse
|
||||
);
|
||||
}
|
||||
|
||||
function getQueriesLinkedToDatasource(dataSourceId) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||
return fetch(`${config.apiUrl}/data-sources/dependent-queries/${dataSourceId}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export const userService = {
|
|||
getAvatar,
|
||||
updateAvatar,
|
||||
updateUserType,
|
||||
updateUserTypeInstance,
|
||||
getUserLimits,
|
||||
changeUserPassword,
|
||||
generateUserPassword,
|
||||
|
|
@ -80,6 +81,16 @@ function updateUserType(userUpdateBody) {
|
|||
return fetch(`${config.apiUrl}/users/user-type`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function updateUserTypeInstance(userUpdateBody) {
|
||||
const requestOptions = {
|
||||
method: 'PATCH',
|
||||
headers: authHeader(),
|
||||
body: JSON.stringify(userUpdateBody),
|
||||
credentials: 'include',
|
||||
};
|
||||
return fetch(`${config.apiUrl}/users/user-type/instance`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function changePassword(currentPassword, newPassword) {
|
||||
const body = { currentPassword, newPassword };
|
||||
const requestOptions = { method: 'PATCH', headers: authHeader(), credentials: 'include', body: JSON.stringify(body) };
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import config from 'config';
|
|||
import { capitalize, isEmpty } from 'lodash';
|
||||
import { Card } from '@/_ui/Card';
|
||||
import { withTranslation, useTranslation } from 'react-i18next';
|
||||
import { camelizeKeys, decamelizeKeys } from 'humps';
|
||||
import { camelizeKeys, decamelizeKeys, decamelize } from 'humps';
|
||||
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
import { useAppVersionStore } from '@/_stores/appVersionStore';
|
||||
|
|
@ -249,14 +249,26 @@ class DataSourceManagerComponent extends React.Component {
|
|||
const scope = this.state?.scope || selectedDataSource?.scope;
|
||||
|
||||
const parsedOptions = Object?.keys(options)?.map((key) => {
|
||||
const keyMeta = dataSourceMeta.options[key];
|
||||
let keyMeta = dataSourceMeta.options[key];
|
||||
let isEncrypted = false;
|
||||
if (keyMeta) {
|
||||
isEncrypted = keyMeta.encrypted;
|
||||
}
|
||||
|
||||
// to resolve any casing mis-match
|
||||
if (decamelize(key) !== key) {
|
||||
const newKey = decamelize(key);
|
||||
isEncrypted = dataSourceMeta.options[newKey]?.encrypted;
|
||||
}
|
||||
|
||||
return {
|
||||
key: key,
|
||||
value: options[key].value,
|
||||
encrypted: keyMeta ? keyMeta.encrypted : false,
|
||||
encrypted: isEncrypted,
|
||||
...(!options[key]?.value && { credential_id: options[key]?.credential_id }),
|
||||
};
|
||||
});
|
||||
|
||||
if (OAuthDs.includes(kind)) {
|
||||
const value = localStorage.getItem('OAuthCode');
|
||||
parsedOptions.push({ key: 'code', value, encrypted: false });
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import SolidIcon from '@/_ui/Icon/SolidIcons';
|
|||
import { SearchBox } from '@/_components/SearchBox';
|
||||
import { DATA_SOURCE_TYPE } from '@/_helpers/constants';
|
||||
import FolderSkeleton from '@/_ui/FolderSkeleton/FolderSkeleton';
|
||||
import Modal from '@/HomePage/Modal';
|
||||
|
||||
export const List = ({ updateSelectedDatasource }) => {
|
||||
const {
|
||||
|
|
@ -28,6 +29,7 @@ export const List = ({ updateSelectedDatasource }) => {
|
|||
const [isDeleteModalVisible, setDeleteModalVisibility] = React.useState(false);
|
||||
const [filteredData, setFilteredData] = useState(dataSources);
|
||||
const [showInput, setShowInput] = useState(false);
|
||||
const [showDependentQueriesInfo, setShowDependentQueriesInfo] = useState(false);
|
||||
|
||||
const darkMode = localStorage.getItem('darkMode') === 'true';
|
||||
|
||||
|
|
@ -50,7 +52,7 @@ export const List = ({ updateSelectedDatasource }) => {
|
|||
setCurrentEnvironment(environments[0]);
|
||||
toggleDataSourceManagerModal(true);
|
||||
updateSelectedDatasource(selectedSource?.name);
|
||||
setDeleteModalVisibility(true);
|
||||
getQueriesLinkedToDatasource(selectedSource);
|
||||
};
|
||||
|
||||
const executeDataSourceDeletion = () => {
|
||||
|
|
@ -74,6 +76,21 @@ export const List = ({ updateSelectedDatasource }) => {
|
|||
});
|
||||
};
|
||||
|
||||
const getQueriesLinkedToDatasource = (selectedSource) => {
|
||||
globalDatasourceService
|
||||
.getQueriesLinkedToDatasource(selectedSource.id)
|
||||
.then((data) => {
|
||||
if (data?.dependent_queries) {
|
||||
setShowDependentQueriesInfo(true);
|
||||
} else {
|
||||
setDeleteModalVisibility(true);
|
||||
}
|
||||
})
|
||||
.catch(({ error }) => {
|
||||
toast.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
const cancelDeleteDataSource = () => {
|
||||
setDeleteModalVisibility(false);
|
||||
};
|
||||
|
|
@ -171,6 +188,16 @@ export const List = ({ updateSelectedDatasource }) => {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Modal
|
||||
title="Dependent queries found!"
|
||||
show={showDependentQueriesInfo}
|
||||
closeModal={() => setShowDependentQueriesInfo(false)}
|
||||
>
|
||||
<div className="mt-3 mb-3">
|
||||
Cannot delete <b>{selectedDataSource?.name ? selectedDataSource.name : 'datasource'}</b> as it is used in the
|
||||
apps
|
||||
</div>
|
||||
</Modal>
|
||||
<ConfirmDialog
|
||||
show={isDeleteModalVisible}
|
||||
message={'You will lose all the queries created from this data source. Do you really want to delete?'}
|
||||
|
|
|
|||
5250
marketplace/package-lock.json
generated
5250
marketplace/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -9,7 +9,7 @@
|
|||
"@types/jest": "^29.5.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.57.0",
|
||||
"@typescript-eslint/parser": "^5.57.0",
|
||||
"aws-sdk": "^2.1326.0",
|
||||
"aws-sdk": "^2.1692.0",
|
||||
"eslint": "^8.37.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-jest": "^27.2.1",
|
||||
|
|
|
|||
5
marketplace/plugins/clickup/.gitignore
vendored
Normal file
5
marketplace/plugins/clickup/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
node_modules
|
||||
lib/*.d.*
|
||||
lib/*.js
|
||||
lib/*.js.map
|
||||
dist/*
|
||||
4
marketplace/plugins/clickup/README.md
Normal file
4
marketplace/plugins/clickup/README.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
# ClickUp
|
||||
|
||||
Documentation on: https://docs.tooljet.com/docs/data-sources/clickup
|
||||
7
marketplace/plugins/clickup/__tests__/index.js
Normal file
7
marketplace/plugins/clickup/__tests__/index.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
const clickup = require('../lib');
|
||||
|
||||
describe('clickup', () => {
|
||||
it.todo('needs tests');
|
||||
});
|
||||
19
marketplace/plugins/clickup/lib/icon.svg
Normal file
19
marketplace/plugins/clickup/lib/icon.svg
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_485_45)">
|
||||
<path d="M4 16.5435L6.88198 14.3135C8.41315 16.332 10.0399 17.2624 11.8507 17.2624C13.6518 17.2624 15.2326 16.343 16.6947 14.3403L19.6179 16.5166C17.5081 19.4044 14.8864 20.9302 11.8507 20.9302C8.82468 20.9302 6.17753 19.4142 4 16.5435Z" fill="url(#paint0_linear_485_45)"/>
|
||||
<path d="M11.8415 6.85133L6.71177 11.3163L4.34058 8.53855L11.8524 2.00004L19.3048 8.5434L16.9228 11.3114L11.8415 6.85133Z" fill="url(#paint1_linear_485_45)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_485_45" x1="4" y1="22.1219" x2="19.6179" y2="22.1219" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#7612FA"/>
|
||||
<stop offset="1" stop-color="#40DDFF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_485_45" x1="4.34058" y1="12.9942" x2="19.3048" y2="12.9942" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FA12E3"/>
|
||||
<stop offset="1" stop-color="#FFD700"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_485_45">
|
||||
<rect width="16" height="20" fill="white" transform="translate(4 2)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
114
marketplace/plugins/clickup/lib/index.ts
Normal file
114
marketplace/plugins/clickup/lib/index.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import { QueryError, QueryResult, QueryService, ConnectionTestResult } from '@tooljet-marketplace/common';
|
||||
import { SourceOptions } from './types';
|
||||
import got, { Headers } from 'got';
|
||||
|
||||
export default class Clickup implements QueryService {
|
||||
authHeader(token: string): Headers {
|
||||
return { Authorization: token };
|
||||
}
|
||||
|
||||
async run(sourceOptions: SourceOptions, queryOptions: any, dataSourceId: string): Promise<QueryResult> {
|
||||
const operation = queryOptions.operation;
|
||||
const apiKey = sourceOptions.apiKey;
|
||||
const baseUrl = 'https://api.clickup.com/api';
|
||||
const path = queryOptions['path'];
|
||||
|
||||
const pathParams = queryOptions['params']['path'];
|
||||
const queryParams = queryOptions['params']['query'];
|
||||
const bodyParams = queryOptions['params']['request'];
|
||||
|
||||
// Replace path params in URL
|
||||
let modifiedPath = path;
|
||||
for (const param of Object.keys(pathParams)) {
|
||||
modifiedPath = modifiedPath.replace(`{${param}}`, pathParams[param]);
|
||||
}
|
||||
|
||||
const url = `${baseUrl}${modifiedPath}`;
|
||||
|
||||
try {
|
||||
let response;
|
||||
|
||||
if (operation === 'get' || operation === 'delete') {
|
||||
response = await got(url, {
|
||||
method: operation,
|
||||
headers: this.authHeader(apiKey),
|
||||
searchParams: queryParams,
|
||||
});
|
||||
} else {
|
||||
// post, put, patch operations
|
||||
const resolvedBodyParams = this.resolveBodyparams(bodyParams);
|
||||
response = await got(url, {
|
||||
method: operation,
|
||||
headers: this.authHeader(apiKey),
|
||||
json: resolvedBodyParams,
|
||||
searchParams: queryParams,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'ok',
|
||||
data: JSON.parse(response.body),
|
||||
};
|
||||
} catch (err) {
|
||||
const errorMessage = err.message || 'An unknown error occurred';
|
||||
const errorDetails: any = {};
|
||||
|
||||
if (err.response) {
|
||||
const { statusCode, body } = err.response;
|
||||
errorDetails.statusCode = statusCode;
|
||||
|
||||
try {
|
||||
const parsedBody = JSON.parse(body);
|
||||
errorDetails.error = parsedBody.err || null;
|
||||
errorDetails.code = parsedBody.ECODE || null;
|
||||
} catch (parseError) {
|
||||
errorDetails.rawBody = body;
|
||||
}
|
||||
}
|
||||
|
||||
throw new QueryError('Query could not be completed', errorMessage, errorDetails);
|
||||
}
|
||||
}
|
||||
|
||||
async testConnection(sourceOptions: SourceOptions): Promise<ConnectionTestResult> {
|
||||
const apiKey = sourceOptions.apiKey;
|
||||
|
||||
try {
|
||||
const response = await got('https://api.clickup.com/api/v2/user', {
|
||||
headers: this.authHeader(apiKey),
|
||||
});
|
||||
|
||||
const data = JSON.parse(response.body);
|
||||
|
||||
if (data?.user?.id) {
|
||||
return {
|
||||
status: 'ok',
|
||||
};
|
||||
} else {
|
||||
throw new QueryError('User information not found', 'Invalid API key or insufficient permissions', {});
|
||||
}
|
||||
} catch (error) {
|
||||
throw new QueryError('Connection could not be established', error.response?.body || error.message, {});
|
||||
}
|
||||
}
|
||||
|
||||
private resolveBodyparams(bodyParams: object): object {
|
||||
if (typeof bodyParams === 'string') {
|
||||
return bodyParams;
|
||||
}
|
||||
|
||||
const expectedResult = {};
|
||||
|
||||
for (const key of Object.keys(bodyParams)) {
|
||||
if (typeof bodyParams[key] === 'object') {
|
||||
for (const subKey of Object.keys(bodyParams[key])) {
|
||||
expectedResult[`${key}[${subKey}]`] = bodyParams[key][subKey];
|
||||
}
|
||||
} else {
|
||||
expectedResult[key] = bodyParams[key];
|
||||
}
|
||||
}
|
||||
|
||||
return expectedResult;
|
||||
}
|
||||
}
|
||||
33
marketplace/plugins/clickup/lib/manifest.json
Normal file
33
marketplace/plugins/clickup/lib/manifest.json
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"$schema": "https://raw.githubusercontent.com/ToolJet/ToolJet/develop/plugins/schemas/manifest.schema.json",
|
||||
"title": "ClickUp datasource",
|
||||
"description": "Clickup plugin for task, list, and doc management",
|
||||
"type": "api",
|
||||
"source": {
|
||||
"name": "ClickUp",
|
||||
"kind": "clickup",
|
||||
"exposedVariables": {
|
||||
"isLoading": false,
|
||||
"data": {},
|
||||
"rawData": {}
|
||||
},
|
||||
"options": {
|
||||
"apiKey": {
|
||||
"type": "string",
|
||||
"encrypted": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaults": {},
|
||||
"properties": {
|
||||
"apiKey": {
|
||||
"label": "API Key",
|
||||
"key": "apiKey",
|
||||
"type": "password",
|
||||
"description": "Enter your Personal API Token"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"apiKey"
|
||||
]
|
||||
}
|
||||
16
marketplace/plugins/clickup/lib/operations.json
Normal file
16
marketplace/plugins/clickup/lib/operations.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"$schema": "https://raw.githubusercontent.com/ToolJet/ToolJet/develop/plugins/schemas/operations.schema.json",
|
||||
"title": "ClickUp datasource",
|
||||
"description": "A schema defining ClickUp datasource",
|
||||
"type": "api",
|
||||
"defaults": {},
|
||||
"properties": {
|
||||
"operation": {
|
||||
"label": "",
|
||||
"key": "clickup_operation",
|
||||
"type": "react-component-api-endpoint",
|
||||
"description": "Single select dropdown for operation",
|
||||
"spec_url": "https://developer.clickup.com/openapi/673cf4cfdca96a0019533cad"
|
||||
}
|
||||
}
|
||||
}
|
||||
3
marketplace/plugins/clickup/lib/types.ts
Normal file
3
marketplace/plugins/clickup/lib/types.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export type SourceOptions = {
|
||||
apiKey: string;
|
||||
};
|
||||
26
marketplace/plugins/clickup/package.json
Normal file
26
marketplace/plugins/clickup/package.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "@tooljet-marketplace/clickup",
|
||||
"version": "1.0.0",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"directories": {
|
||||
"lib": "lib",
|
||||
"test": "__tests__"
|
||||
},
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "echo \"Error: run tests from root\" && exit 1",
|
||||
"build": "ncc build lib/index.ts -o dist",
|
||||
"watch": "ncc build lib/index.ts -o dist --watch"
|
||||
},
|
||||
"homepage": "https://github.com/tooljet/tooljet#readme",
|
||||
"dependencies": {
|
||||
"@tooljet-marketplace/common": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^4.7.4",
|
||||
"@vercel/ncc": "^0.34.0"
|
||||
}
|
||||
}
|
||||
11
marketplace/plugins/clickup/tsconfig.json
Normal file
11
marketplace/plugins/clickup/tsconfig.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "lib"
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
|
|
@ -15,6 +15,9 @@
|
|||
"options": {
|
||||
"url": {
|
||||
"type": "string"
|
||||
},
|
||||
"personal_token": {
|
||||
"encrypted": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -57,11 +60,12 @@
|
|||
"key": "personal_token",
|
||||
"type": "password",
|
||||
"description": "Enter your api token",
|
||||
"hint": "You can generate a personal access token from your Jira account 'Manage account'."
|
||||
"hint": "You can generate a personal access token from your Jira account 'Manage account'.",
|
||||
"encrypted": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"url"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -146,7 +146,7 @@
|
|||
"height": "36px"
|
||||
},
|
||||
"withPayload": {
|
||||
"label": "Include metadata",
|
||||
"label": "Include payload",
|
||||
"key": "withPayload",
|
||||
"type": "codehinter",
|
||||
"description": "Whether to return payload values.",
|
||||
|
|
@ -163,4 +163,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,16 @@ export default class Supabase implements QueryService {
|
|||
}
|
||||
|
||||
if (error) {
|
||||
throw new QueryError('Query could not be completed', error, {});
|
||||
const errorMessage = error?.message || "An unknown error occurred.";
|
||||
let errorDetails: any = {};
|
||||
|
||||
const supabaseError = error as any;
|
||||
const { code, hint } = supabaseError;
|
||||
|
||||
errorDetails.code = code;
|
||||
errorDetails.hint = hint;
|
||||
|
||||
throw new QueryError('Query could not be completed', errorMessage, errorDetails);
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import readDir from 'recursive-readdir';
|
|||
import { resolve as _resolve } from 'path';
|
||||
import aws from 'aws-sdk';
|
||||
import { lookup } from 'mime-types';
|
||||
import chalk from 'chalk';
|
||||
|
||||
const { config, S3 } = aws;
|
||||
const __dirname = _resolve();
|
||||
|
|
@ -30,7 +31,16 @@ const generateFileKey = (fileName) => {
|
|||
const s3 = new S3();
|
||||
|
||||
const uploadToS3 = async () => {
|
||||
const start = Date.now();
|
||||
const errors = [];
|
||||
let successCount = 0;
|
||||
|
||||
console.log(chalk.cyanBright('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
||||
console.log(chalk.cyanBright('📤 S3 ASSETS UPLOADER'));
|
||||
console.log(chalk.cyanBright('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'));
|
||||
|
||||
try {
|
||||
console.log(`[${new Date().toLocaleTimeString()}] ℹ Scanning directory for files...`);
|
||||
const fileArray = await getDirectoryFilesRecursive(directoryPath, [
|
||||
'common',
|
||||
'.DS_Store',
|
||||
|
|
@ -43,30 +53,76 @@ const uploadToS3 = async () => {
|
|||
'tsconfig.json',
|
||||
]);
|
||||
|
||||
fileArray.map((file) => {
|
||||
// Configuring parameters for S3 Object
|
||||
const S3params = {
|
||||
Bucket: process.env.AWS_BUCKET,
|
||||
Body: createReadStream(file),
|
||||
Key: generateFileKey(file),
|
||||
ContentType: lookup(file),
|
||||
ContentEncoding: 'utf-8',
|
||||
CacheControl: 'immutable,max-age=31536000,public',
|
||||
};
|
||||
s3.upload(S3params, function (err, data) {
|
||||
if (err) {
|
||||
// Set the exit code while letting
|
||||
// the process exit gracefully.
|
||||
console.error(err);
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log(`Assets uploaded to S3: `, data);
|
||||
}
|
||||
console.log(`[${new Date().toLocaleTimeString()}] ℹ Found ${fileArray.length} files to upload`);
|
||||
console.log(`[${new Date().toLocaleTimeString()}] ℹ Target bucket: ${process.env.AWS_BUCKET}\n`);
|
||||
|
||||
const uploadPromises = fileArray.map((file, index) => {
|
||||
return new Promise((resolve) => {
|
||||
const S3params = {
|
||||
Bucket: process.env.AWS_BUCKET,
|
||||
Body: createReadStream(file),
|
||||
Key: generateFileKey(file),
|
||||
ContentType: lookup(file) || 'application/octet-stream',
|
||||
ContentEncoding: 'utf-8',
|
||||
CacheControl: 'immutable,max-age=31536000,public',
|
||||
};
|
||||
|
||||
s3.upload(S3params, function (err, data) {
|
||||
const indexStr = `[${(index + 1).toString().padStart(2, '0')}/${fileArray.length}]`;
|
||||
if (err) {
|
||||
console.log(chalk.redBright(`${indexStr} ❌ Failed to upload: ${file}`));
|
||||
console.error(chalk.gray(`↳ ${err.message}`));
|
||||
errors.push({ file, message: err.message });
|
||||
} else {
|
||||
console.log(chalk.greenBright(`${indexStr} ✅ Uploaded: ${file}`));
|
||||
console.log(
|
||||
chalk.gray(
|
||||
JSON.stringify(
|
||||
{
|
||||
ETag: data.ETag,
|
||||
Location: data.Location,
|
||||
Key: data.Key,
|
||||
Bucket: data.Bucket,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
);
|
||||
successCount++;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await Promise.all(uploadPromises);
|
||||
|
||||
const duration = ((Date.now() - start) / 1000).toFixed(1);
|
||||
|
||||
console.log(chalk.cyanBright('\n━━━━━━━━━━━━━━━ UPLOAD SUMMARY ━━━━━━━━━━━━━━━━━'));
|
||||
if (errors.length > 0) {
|
||||
console.log(`[${new Date().toLocaleTimeString()}] ⚠️ Upload completed with ${errors.length} error(s)`);
|
||||
} else {
|
||||
console.log(`[${new Date().toLocaleTimeString()}] 🎉 All files uploaded successfully`);
|
||||
}
|
||||
console.log(`[${new Date().toLocaleTimeString()}] ✅ Successfully uploaded: ${successCount}/${fileArray.length} files`);
|
||||
console.log(`[${new Date().toLocaleTimeString()}] ❌ Failed uploads: ${errors.length}/${fileArray.length} files`);
|
||||
console.log(`[${new Date().toLocaleTimeString()}] ℹ Total time: ${duration}s`);
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.log(chalk.cyanBright('\n━━━━━━━━━━━━━━━ ERROR DETAILS ━━━━━━━━━━━━━━━━━'));
|
||||
errors.forEach((err, idx) => {
|
||||
console.log(chalk.red(`Error #${idx + 1}: ${err.file}`));
|
||||
console.log(chalk.gray(` ↳ ${err.message}`));
|
||||
});
|
||||
process.exitCode = 1;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(chalk.bgRed.white('❌ Script failed with error:'));
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
uploadToS3();
|
||||
uploadToS3();
|
||||
|
|
@ -57,7 +57,18 @@ export default class FirestoreQueryService implements QueryService {
|
|||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
throw new QueryError('Query could not be completed', error.message, {});
|
||||
const errorMessage = error.message || "An unknown error occurred.";
|
||||
let errorDetails: any = {};
|
||||
|
||||
if (error && error instanceof Error) {
|
||||
const firestoreError = error as any;
|
||||
const { code, name } = firestoreError;
|
||||
|
||||
errorDetails.code = code as string;
|
||||
errorDetails.name = name;
|
||||
}
|
||||
|
||||
throw new QueryError('Query could not be completed', errorMessage, errorDetails);
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -46,8 +46,17 @@ export default class GRPC implements QueryService {
|
|||
metadata.add(sourceOptions.grpc_apikey_key, sourceOptions.grpc_apikey_value);
|
||||
}
|
||||
|
||||
let jsonMessage = {};
|
||||
if (queryOptions.jsonMessage) {
|
||||
try {
|
||||
jsonMessage = JSON.parse(queryOptions.jsonMessage);
|
||||
} catch (e) {
|
||||
throw new QueryError('Invalid JSON message', {}, {});
|
||||
}
|
||||
}
|
||||
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
clientStub[rpc]({}, metadata, (err: any, response: any) => {
|
||||
clientStub[rpc](jsonMessage, metadata, (err: any, response: any) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,5 +11,6 @@ export type SourceOptions = {
|
|||
export type QueryOptions = {
|
||||
operation: string;
|
||||
serviceName: string;
|
||||
jsonMessage: string;
|
||||
rpc: string;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@
|
|||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"encrypted": true
|
||||
},
|
||||
"connectionLimit": {
|
||||
"type": "string"
|
||||
|
|
@ -83,7 +84,8 @@
|
|||
"label": "Password",
|
||||
"key": "password",
|
||||
"type": "password",
|
||||
"description": "Enter password"
|
||||
"description": "Enter password",
|
||||
"encrypted": true
|
||||
},
|
||||
"connectionLimit": {
|
||||
"label": "Connection Limit",
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
{
|
||||
"$schema": "https://raw.githubusercontent.com/ToolJet/ToolJet/develop/plugins/schemas/operations.schema.json",
|
||||
"title": "Stripe datasource",
|
||||
"description": "A schema defining stripe datasource",
|
||||
"type": "api",
|
||||
"defaults": {},
|
||||
"properties": {
|
||||
"operation": {
|
||||
"label": "",
|
||||
"key": "stripe_operation",
|
||||
"type": "react-component-api-endpoint",
|
||||
"description": "Single select dropdown for operation",
|
||||
"specUrl": "https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.json"
|
||||
}
|
||||
"$schema": "https://raw.githubusercontent.com/ToolJet/ToolJet/develop/plugins/schemas/operations.schema.json",
|
||||
"title": "Stripe datasource",
|
||||
"description": "A schema defining stripe datasource",
|
||||
"type": "api",
|
||||
"defaults": {},
|
||||
"properties": {
|
||||
"operation": {
|
||||
"label": "",
|
||||
"key": "stripe_operation",
|
||||
"type": "react-component-api-endpoint",
|
||||
"description": "Single select dropdown for operation",
|
||||
"spec_url": "https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
3
plugins/packages/woocommerce/.gitignore
vendored
3
plugins/packages/woocommerce/.gitignore
vendored
|
|
@ -1,5 +1,4 @@
|
|||
node_modules
|
||||
lib/*.d.*
|
||||
lib/*.js
|
||||
lib/*.js.map
|
||||
lib/operations.json
|
||||
lib/*.js.map
|
||||
105
plugins/packages/woocommerce/lib/operations.json
Normal file
105
plugins/packages/woocommerce/lib/operations.json
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
{
|
||||
"title": "Woocommerce datasource",
|
||||
"description": "A schema defining Woocommerce datasource",
|
||||
"type": "api",
|
||||
"defaults": {},
|
||||
"properties": {
|
||||
"resource": {
|
||||
"label": "Resource",
|
||||
"key": "resource",
|
||||
"className": "col-md-4",
|
||||
"type": "dropdown-component-flip",
|
||||
"description": "Resource select",
|
||||
"list": [
|
||||
{ "value": "product", "name": "Product" },
|
||||
{ "value": "customer", "name": "Customer" },
|
||||
{ "value": "order", "name": "Order" },
|
||||
{ "value": "coupon", "name": "Coupon" }
|
||||
]
|
||||
},
|
||||
"customer": {
|
||||
"operation": {
|
||||
"label": "Operation",
|
||||
"key": "operation",
|
||||
"type": "dropdown-component-flip",
|
||||
"description": "Single select dropdown for operation",
|
||||
"list": [
|
||||
{ "value": "list_customer", "name": "List all customers" },
|
||||
{ "value": "update_customer", "name": "Update a customer" },
|
||||
{ "value": "delete_customer", "name": "Delete a customer" },
|
||||
{ "value": "batch_update_customer", "name": "Batch update customers" },
|
||||
{ "value": "create_customer", "name": "Create a customer" },
|
||||
{ "value": "retrieve_customer", "name": "Retrieve a customer" }
|
||||
]
|
||||
},
|
||||
"list_customer": {
|
||||
"page": {
|
||||
"label": "Page",
|
||||
"key": "page",
|
||||
"type": "codehinter",
|
||||
"description": "Enter page",
|
||||
"width": "320px",
|
||||
"height": "36px",
|
||||
"className": "codehinter-plugins",
|
||||
"placeholder": "",
|
||||
"lineNumbers": false
|
||||
},
|
||||
"context": {
|
||||
"label": "Context",
|
||||
"key": "context",
|
||||
"type": "codehinter",
|
||||
"description": "Enter context",
|
||||
"width": "320px",
|
||||
"height": "36px",
|
||||
"className": "codehinter-plugins",
|
||||
"placeholder": "",
|
||||
"lineNumbers": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"product": {
|
||||
"operation": {
|
||||
"label": "Operation",
|
||||
"key": "operation",
|
||||
"type": "dropdown-component-flip",
|
||||
"description": "Single select dropdown for operation",
|
||||
"list": [
|
||||
{ "value": "list_product", "name": "List all products" },
|
||||
{ "value": "update_product", "name": "Update a product" },
|
||||
{ "value": "delete_product", "name": "Delete a product" },
|
||||
{ "value": "batch_update_product", "name": "Batch update products" },
|
||||
{ "value": "create_product", "name": "Create a product" },
|
||||
{ "value": "retrieve_product", "name": "Retrieve a product" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"order": {
|
||||
"operation": {
|
||||
"label": "Operation",
|
||||
"key": "operation",
|
||||
"type": "dropdown-component-flip",
|
||||
"description": "Single select dropdown for operation",
|
||||
"list": [
|
||||
{ "value": "list_order", "name": "List all orders" },
|
||||
{ "value": "update_order", "name": "Update an order" },
|
||||
{ "value": "delete_order", "name": "Delete an order" },
|
||||
{ "value": "batch_update_order", "name": "Batch update orders" },
|
||||
{ "value": "create_order", "name": "Create an order" },
|
||||
{ "value": "retrieve_order", "name": "Retrieve an order" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"coupon": {
|
||||
"operation": {
|
||||
"label": "Operation",
|
||||
"key": "operation",
|
||||
"type": "dropdown-component-flip",
|
||||
"description": "Single select dropdown for operation",
|
||||
"list": [
|
||||
{ "value": "list_coupon", "name": "List all coupons" },
|
||||
{ "value": "create_coupon", "name": "Create a coupon" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
3.13.0
|
||||
3.14.0
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 30dbfa754562d00f8d64181d5006e113798bd668
|
||||
Subproject commit f70ac83c38e0a8b44aeb2a0fb2059690eb5e2f46
|
||||
16
server/migrations/1746520805456-AddResourceDataAudit.ts
Normal file
16
server/migrations/1746520805456-AddResourceDataAudit.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
|
||||
|
||||
export class AddResourceDataAudit1746520805456 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.addColumn(
|
||||
'audit_logs',
|
||||
new TableColumn({
|
||||
name: 'resource_data',
|
||||
type: 'json',
|
||||
isNullable: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { MigrationInterface, QueryRunner, TableUnique } from "typeorm";
|
||||
|
||||
export class AddPluginIdUniqueConstraint1747133448781 implements MigrationInterface {
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.createUniqueConstraint(
|
||||
"plugins",
|
||||
new TableUnique({
|
||||
name: "UQ_plugin_pluginId",
|
||||
columnNames: ["plugin_id"],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.dropUniqueConstraint(
|
||||
"plugins",
|
||||
"UQ_plugin_pluginId"
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
redis-server /etc/redis/redis.conf &
|
||||
|
||||
# Fix ownership and permissions
|
||||
chown -R postgres:postgres /var/lib/postgresql /var/run/postgresql
|
||||
chmod 0700 /var/lib/postgresql/13/main
|
||||
|
|
|
|||
|
|
@ -221,5 +221,13 @@
|
|||
"id": "azurerepos",
|
||||
"author": "Tooljet",
|
||||
"timestamp": "Mon, 23 Dec 2024 11:57:30 GMT"
|
||||
},
|
||||
{
|
||||
"name": "ClickUp",
|
||||
"description": "ClickUp plugin for task, list, and doc management",
|
||||
"version": "1.0.0",
|
||||
"id": "clickup",
|
||||
"author": "Tooljet",
|
||||
"timestamp": "Wed, 16 Apr 2025 15:31:38 GMT"
|
||||
}
|
||||
]
|
||||
|
|
@ -23,6 +23,9 @@ export class AuditLog extends BaseEntity {
|
|||
@Column({ name: 'resource_type', type: 'enum', enum: MODULES })
|
||||
resourceType: MODULES;
|
||||
|
||||
@Column('simple-json', { name: 'resource_data' })
|
||||
resourceData;
|
||||
|
||||
@Column({ name: 'action_type' })
|
||||
actionType: string;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,9 +6,11 @@ import {
|
|||
OneToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
Unique
|
||||
} from 'typeorm';
|
||||
import { File } from 'src/entities/file.entity';
|
||||
|
||||
@Unique(['pluginId'])
|
||||
@Entity({ name: 'plugins' })
|
||||
export class Plugin {
|
||||
@PrimaryGeneratedColumn()
|
||||
|
|
|
|||
|
|
@ -7,6 +7,6 @@ export interface IAuditLogService {
|
|||
perform(
|
||||
{ userId, organizationId, resourceId, resourceType, actionType, resourceName, metadata }: AuditLogFields,
|
||||
manager?: EntityManager
|
||||
): Promise<AuditLog>;
|
||||
): Promise<AuditLog[]>;
|
||||
findPerPage(user: User, query: AuditLogsQuery): Promise<any>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,10 +18,12 @@ export interface AuditLogFields {
|
|||
organizationId: string;
|
||||
resourceId: string;
|
||||
resourceType: MODULES;
|
||||
resourceData?: object;
|
||||
actionType: string;
|
||||
resourceName?: string;
|
||||
ipAddress?: string;
|
||||
metadata?: object;
|
||||
organizationIds?: Array<string>;
|
||||
}
|
||||
|
||||
export interface Features {
|
||||
|
|
|
|||
|
|
@ -28,12 +28,15 @@ export const FEATURES: FeaturesConfig = {
|
|||
},
|
||||
[FEATURE_KEY.FORGOT_PASSWORD]: {
|
||||
isPublic: true,
|
||||
auditLogsKey: 'USER_PASSWORD_FORGOT',
|
||||
},
|
||||
[FEATURE_KEY.RESET_PASSWORD]: {
|
||||
isPublic: true,
|
||||
auditLogsKey: 'USER_PASSWORD_RESET',
|
||||
},
|
||||
[FEATURE_KEY.OAUTH_SIGN_IN]: {
|
||||
isPublic: true,
|
||||
auditLogsKey: 'USER_LOGIN',
|
||||
},
|
||||
[FEATURE_KEY.OAUTH_OPENID_CONFIGS]: {
|
||||
isPublic: true,
|
||||
|
|
@ -43,6 +46,7 @@ export const FEATURES: FeaturesConfig = {
|
|||
},
|
||||
[FEATURE_KEY.OAUTH_COMMON_SIGN_IN]: {
|
||||
isPublic: true,
|
||||
auditLogsKey: 'USER_LOGIN',
|
||||
},
|
||||
[FEATURE_KEY.OAUTH_SAML_RESPONSE]: {
|
||||
isPublic: true,
|
||||
|
|
|
|||
|
|
@ -124,6 +124,9 @@ export class AuthService implements IAuthService {
|
|||
organizationId: organization.id,
|
||||
resourceId: user.id,
|
||||
resourceName: user.email,
|
||||
resourceData: {
|
||||
auth_method: 'password',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -184,6 +187,13 @@ export class AuthService implements IAuthService {
|
|||
forgotPasswordToken: null,
|
||||
passwordRetryCount: 0,
|
||||
});
|
||||
const auditLogEntry = {
|
||||
userId: user.id,
|
||||
organizationId: user.defaultOrganizationId,
|
||||
resourceId: user.id,
|
||||
resourceName: user.email,
|
||||
};
|
||||
RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, auditLogEntry);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -195,6 +205,13 @@ export class AuthService implements IAuthService {
|
|||
}
|
||||
const forgotPasswordToken = uuid.v4();
|
||||
await this.userRepository.updateOne(user.id, { forgotPasswordToken });
|
||||
const auditLogEntry = {
|
||||
userId: user.id,
|
||||
organizationId: user.defaultOrganizationId,
|
||||
resourceId: user.id,
|
||||
resourceName: user.email,
|
||||
};
|
||||
RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, auditLogEntry);
|
||||
this.eventEmitter.emit('emailEvent', {
|
||||
type: EMAIL_EVENTS.SEND_PASSWORD_RESET_EMAIL,
|
||||
payload: {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { UserAllPermissions } from '@modules/app/types';
|
|||
import { FEATURE_KEY } from '../constants';
|
||||
import { DataSource } from '@entities/data_source.entity';
|
||||
import { MODULES } from '@modules/app/constants/modules';
|
||||
import { getTooljetEdition } from '@helpers/utils.helper';
|
||||
|
||||
type Subjects = InferSubjects<typeof DataSource> | 'all';
|
||||
export type FeatureAbility = Ability<[FEATURE_KEY, Subjects]>;
|
||||
|
|
@ -33,10 +34,13 @@ export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
|
|||
const isAllViewable = !!resourcePermissions?.isAllUsable;
|
||||
|
||||
const dataSourceId = request?.tj_resource_id;
|
||||
|
||||
const toolJetEdition = getTooljetEdition();
|
||||
// Oauth end points available to all
|
||||
can(FEATURE_KEY.GET_OAUTH2_BASE_URL, DataSource);
|
||||
can(FEATURE_KEY.AUTHORIZE, DataSource);
|
||||
if ((toolJetEdition == 'ee' && superAdmin) || (toolJetEdition !== 'ee' && isAdmin)) {
|
||||
can(FEATURE_KEY.QUERIES_DATASOURCE_LINKED_TO_MARKETPLACE_PLUGIN, DataSource);
|
||||
}
|
||||
|
||||
if (isBuilder) {
|
||||
// Only builder can do scope change, Get call is there on app builder
|
||||
|
|
@ -56,6 +60,7 @@ export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
|
|||
FEATURE_KEY.TEST_CONNECTION,
|
||||
FEATURE_KEY.SCOPE_CHANGE,
|
||||
FEATURE_KEY.GET_FOR_APP,
|
||||
FEATURE_KEY.QUERIES_LINKED_TO_DATASOURCE,
|
||||
],
|
||||
DataSource
|
||||
);
|
||||
|
|
@ -70,7 +75,7 @@ export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
|
|||
);
|
||||
|
||||
if (isCanDelete) {
|
||||
can(FEATURE_KEY.DELETE, DataSource);
|
||||
can([FEATURE_KEY.DELETE, FEATURE_KEY.QUERIES_LINKED_TO_DATASOURCE], DataSource);
|
||||
}
|
||||
if (isCanCreate) {
|
||||
can(FEATURE_KEY.CREATE, DataSource);
|
||||
|
|
|
|||
|
|
@ -20,5 +20,7 @@ export const FEATURES: FeaturesConfig = {
|
|||
[FEATURE_KEY.GET_OAUTH2_BASE_URL]: {},
|
||||
[FEATURE_KEY.AUTHORIZE]: {},
|
||||
[FEATURE_KEY.GET_FOR_APP]: {},
|
||||
[FEATURE_KEY.QUERIES_LINKED_TO_DATASOURCE]: {},
|
||||
[FEATURE_KEY.QUERIES_DATASOURCE_LINKED_TO_MARKETPLACE_PLUGIN]: {},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ export enum FEATURE_KEY {
|
|||
TEST_CONNECTION = 'TEST_CONNECTION',
|
||||
GET_OAUTH2_BASE_URL = 'GET_OAUTH2_BASE_URL',
|
||||
AUTHORIZE = 'AUTHORIZE',
|
||||
QUERIES_LINKED_TO_DATASOURCE = 'QUERIES_LINKED_TO_DATASOURCE',
|
||||
QUERIES_DATASOURCE_LINKED_TO_MARKETPLACE_PLUGIN = 'QUERIES_DATASOURCE_LINKED_TO_MARKETPLACE_PLUGIN',
|
||||
}
|
||||
|
||||
export enum DataSourceTypes {
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ export class DataSourcesController implements IDataSourcesController {
|
|||
|
||||
@InitFeature(FEATURE_KEY.GET_OAUTH2_BASE_URL)
|
||||
@UseGuards(FeatureAbilityGuard)
|
||||
@Get('fetch-oauth2-base-url')
|
||||
@Post('fetch-oauth2-base-url')
|
||||
getAuthUrl(@Body() getDataSourceOauthUrlDto: GetDataSourceOauthUrlDto) {
|
||||
return this.dataSourcesService.getAuthUrl(getDataSourceOauthUrlDto);
|
||||
}
|
||||
|
|
@ -129,6 +129,20 @@ export class DataSourcesController implements IDataSourcesController {
|
|||
return;
|
||||
}
|
||||
|
||||
@InitFeature(FEATURE_KEY.QUERIES_DATASOURCE_LINKED_TO_MARKETPLACE_PLUGIN)
|
||||
@UseGuards(FeatureAbilityGuard)
|
||||
@Get('dependent-queries/marketplace-plugin/:plugin_id')
|
||||
async findDatasourcesAndQueriesOfMarketplacePlugin(@User() user: UserEntity, @Param('plugin_id') pluginId) {
|
||||
return await this.dataSourcesService.findDatasourcesAndQueriesOfMarketplacePlugin(pluginId);
|
||||
}
|
||||
|
||||
@InitFeature(FEATURE_KEY.QUERIES_LINKED_TO_DATASOURCE)
|
||||
@UseGuards(FeatureAbilityGuard)
|
||||
@Get('dependent-queries/:datasource_id')
|
||||
async findQueriesLinkedToDatasource(@User() user: UserEntity, @Param('datasource_id') datasourceId: string) {
|
||||
return await this.dataSourcesService.findQueriesLinkedToDatasource(datasourceId);
|
||||
}
|
||||
|
||||
@InitFeature(FEATURE_KEY.AUTHORIZE)
|
||||
@UseGuards(FeatureAbilityGuard)
|
||||
@Post('decrypt')
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { InstanceSettingsModule } from '@modules/instance-settings/module';
|
|||
import { VersionRepository } from '@modules/versions/repository';
|
||||
import { AppsRepository } from '@modules/apps/repository';
|
||||
import { TooljetDbModule } from '@modules/tooljet-db/module';
|
||||
import { OrganizationRepository } from '@modules/organizations/repository';
|
||||
import { SessionModule } from '@modules/session/module';
|
||||
import { SampleDBScheduler } from './schedulers/sample-db.scheduler';
|
||||
|
||||
|
|
@ -21,6 +22,7 @@ export class DataSourcesModule {
|
|||
const { DataSourcesUtilService } = await import(`${importPath}/data-sources/util.service`);
|
||||
const { PluginsServiceSelector } = await import(`${importPath}/data-sources/services/plugin-selector.service`);
|
||||
const { SampleDataSourceService } = await import(`${importPath}/data-sources/services/sample-ds.service`);
|
||||
const { OrganizationsService } = await import(`${importPath}/organizations/service`);
|
||||
|
||||
return {
|
||||
module: DataSourcesModule,
|
||||
|
|
@ -42,6 +44,8 @@ export class DataSourcesModule {
|
|||
PluginsRepository,
|
||||
SampleDataSourceService,
|
||||
FeatureAbilityFactory,
|
||||
OrganizationsService,
|
||||
OrganizationRepository,
|
||||
SampleDBScheduler,
|
||||
],
|
||||
controllers: [DataSourcesController],
|
||||
|
|
|
|||
|
|
@ -168,4 +168,26 @@ export class DataSourcesRepository extends Repository<DataSource> {
|
|||
});
|
||||
}, manager || this.manager);
|
||||
}
|
||||
|
||||
getDatasourceByPluginId(pluginId: string) {
|
||||
return dbTransactionWrap((manager: EntityManager) => {
|
||||
return manager.find(DataSource, {
|
||||
where: {
|
||||
pluginId: pluginId,
|
||||
},
|
||||
relations: ['dataQueries'],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getQueriesByDatasourceId(datasourceId) {
|
||||
return dbTransactionWrap((manager: EntityManager) => {
|
||||
return manager.find(DataSource, {
|
||||
where: {
|
||||
id: datasourceId,
|
||||
},
|
||||
relations: ['dataQueries'],
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import { GetQueryVariables, UpdateOptions } from './types';
|
|||
import { DataSource } from '@entities/data_source.entity';
|
||||
import { PluginsServiceSelector } from './services/plugin-selector.service';
|
||||
import { IDataSourcesService } from './interfaces/IService';
|
||||
// import { FEATURE_KEY } from './constants';
|
||||
import { OrganizationsService } from '@modules/organizations/service';
|
||||
import { RequestContext } from '@modules/request-context/service';
|
||||
import { AUDIT_LOGS_REQUEST_CONTEXT_KEY } from '@modules/app/constants';
|
||||
|
||||
|
|
@ -30,7 +32,8 @@ export class DataSourcesService implements IDataSourcesService {
|
|||
protected readonly dataSourcesUtilService: DataSourcesUtilService,
|
||||
protected readonly abilityService: AbilityService,
|
||||
protected readonly appEnvironmentsUtilService: AppEnvironmentUtilService,
|
||||
protected readonly pluginsServiceSelector: PluginsServiceSelector
|
||||
protected readonly pluginsServiceSelector: PluginsServiceSelector,
|
||||
protected readonly organizationsService: OrganizationsService
|
||||
) {}
|
||||
|
||||
async getForApp(query: GetQueryVariables, user: User): Promise<{ data_sources: object[] }> {
|
||||
|
|
@ -43,7 +46,6 @@ export class DataSourcesService implements IDataSourcesService {
|
|||
const dataSources = await this.dataSourcesRepository.allGlobalDS(userPermissions, user.organizationId, query ?? {});
|
||||
let staticDataSources = await this.dataSourcesRepository.getAllStaticDataSources(query.appVersionId);
|
||||
|
||||
|
||||
if (!shouldIncludeWorkflows) {
|
||||
// remove workflowsdefault data source from static data sources
|
||||
staticDataSources = staticDataSources.filter((dataSource) => dataSource.kind !== 'workflows');
|
||||
|
|
@ -176,6 +178,12 @@ export class DataSourcesService implements IDataSourcesService {
|
|||
if (dataSource.type === DataSourceTypes.SAMPLE) {
|
||||
throw new BadRequestException('Cannot delete sample data source');
|
||||
}
|
||||
|
||||
const result = await this.findQueriesLinkedToDatasource(dataSourceId);
|
||||
if (result.dependent_queries) {
|
||||
throw new BadRequestException(`Datasource can't be deleted, queries are in use`);
|
||||
}
|
||||
|
||||
await this.dataSourcesRepository.delete(dataSourceId);
|
||||
|
||||
// Setting data for audit logs
|
||||
|
|
@ -243,4 +251,30 @@ export class DataSourcesService implements IDataSourcesService {
|
|||
await this.dataSourcesUtilService.authorizeOauth2(dataSource, code, user.id, environmentId, user.organizationId);
|
||||
return;
|
||||
}
|
||||
|
||||
async findQueriesLinkedToDatasource(datasourceId: string) {
|
||||
const dataSourceDetails = await this.dataSourcesRepository.getQueriesByDatasourceId(datasourceId);
|
||||
if (dataSourceDetails.length == 0) return { datasources: 0, dependent_queries: 0 };
|
||||
|
||||
const queries = [];
|
||||
dataSourceDetails.forEach((datasourceDetail) => {
|
||||
const { dataQueries = [] } = datasourceDetail;
|
||||
if (dataQueries.length) queries.push(...dataQueries);
|
||||
});
|
||||
|
||||
return { datasources: dataSourceDetails.length, dependent_queries: queries.length };
|
||||
}
|
||||
|
||||
async findDatasourcesAndQueriesOfMarketplacePlugin(pluginId: string) {
|
||||
const dataSourcesByMarketplacePlugin = await this.dataSourcesRepository.getDatasourceByPluginId(pluginId);
|
||||
if (!dataSourcesByMarketplacePlugin.length) return { dependent_queries: 0 };
|
||||
|
||||
const queries = [];
|
||||
dataSourcesByMarketplacePlugin?.forEach((datasource) => {
|
||||
if (datasource.dataQueries.length) queries.push(...datasource.dataQueries);
|
||||
});
|
||||
return {
|
||||
dependent_queries: queries.length,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ interface Features {
|
|||
[FEATURE_KEY.GET_OAUTH2_BASE_URL]: FeatureConfig;
|
||||
[FEATURE_KEY.AUTHORIZE]: FeatureConfig;
|
||||
[FEATURE_KEY.GET_FOR_APP]: FeatureConfig;
|
||||
[FEATURE_KEY.QUERIES_LINKED_TO_DATASOURCE]: FeatureConfig;
|
||||
[FEATURE_KEY.QUERIES_DATASOURCE_LINKED_TO_MARKETPLACE_PLUGIN]: FeatureConfig;
|
||||
}
|
||||
|
||||
export interface FeaturesConfig {
|
||||
|
|
|
|||
|
|
@ -189,17 +189,16 @@ export class DataSourcesUtilService implements IDataSourcesUtilService {
|
|||
/*
|
||||
Basic plan customer. lets update all environment options.
|
||||
this will help us to run the queries successfully when the user buys enterprise plan
|
||||
*/
|
||||
await Promise.all(
|
||||
allEnvs.map(async (envToUpdate) => {
|
||||
dataSource.options = (
|
||||
await this.appEnvironmentUtilService.getOptions(dataSourceId, organizationId, envToUpdate.id)
|
||||
).options;
|
||||
*/
|
||||
|
||||
const newOptions = await this.parseOptionsForUpdate(dataSource, options, manager);
|
||||
await this.appEnvironmentUtilService.updateOptions(newOptions, envToUpdate.id, dataSource.id, manager);
|
||||
})
|
||||
);
|
||||
const newOptions = await this.parseOptionsForUpdate(dataSource, options, manager);
|
||||
for (const env of allEnvs) {
|
||||
dataSource.options = (
|
||||
await this.appEnvironmentUtilService.getOptions(dataSourceId, organizationId, env.id)
|
||||
).options;
|
||||
|
||||
await this.appEnvironmentUtilService.updateOptions(newOptions, env.id, dataSource.id, manager);
|
||||
}
|
||||
}
|
||||
const updatableParams = {
|
||||
id: dataSourceId,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export const FEATURES: FeaturesConfig = {
|
|||
[MODULES.ONBOARDING]: {
|
||||
[FEATURE_KEY.ACTIVATE_ACCOUNT]: {
|
||||
isPublic: true,
|
||||
auditLogsKey: 'USER_SIGNUP',
|
||||
}, // Account Activation
|
||||
[FEATURE_KEY.SETUP_SUPER_ADMIN]: {
|
||||
isPublic: true,
|
||||
|
|
@ -15,6 +16,7 @@ export const FEATURES: FeaturesConfig = {
|
|||
}, // Signup
|
||||
[FEATURE_KEY.ACCEPT_INVITE]: {
|
||||
isPublic: true,
|
||||
auditLogsKey: 'USER_INVITE_REDEEM',
|
||||
}, // Accept Invitation
|
||||
[FEATURE_KEY.RESEND_INVITE]: {
|
||||
isPublic: true,
|
||||
|
|
@ -27,6 +29,7 @@ export const FEATURES: FeaturesConfig = {
|
|||
}, // Verify Organization Token
|
||||
[FEATURE_KEY.SETUP_ACCOUNT_FROM_TOKEN]: {
|
||||
isPublic: true,
|
||||
auditLogsKey: 'USER_SIGNUP',
|
||||
}, // Setup Account From Token
|
||||
[FEATURE_KEY.CHECK_WORKSPACE_UNIQUENESS]: {
|
||||
isPublic: true,
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ export class OnboardingService implements IOnboardingService {
|
|||
const userParams = { email, password, firstName, lastName };
|
||||
|
||||
// Find the default workspace
|
||||
const defaultWorkspace = await this.organizationRepository. getDefaultWorkspaceOfInstance();
|
||||
const defaultWorkspace = await this.organizationRepository.getDefaultWorkspaceOfInstance();
|
||||
|
||||
if (existingUser) {
|
||||
// Handling instance and workspace level signup for existing user
|
||||
|
|
@ -133,7 +133,7 @@ export class OnboardingService implements IOnboardingService {
|
|||
manager
|
||||
);
|
||||
} else {
|
||||
if(defaultWorkspace && !signingUpOrganization) {
|
||||
if (defaultWorkspace && !signingUpOrganization) {
|
||||
return await this.onboardingUtilService.createUserInDefaultWorkspace(
|
||||
userParams,
|
||||
defaultWorkspace,
|
||||
|
|
@ -263,7 +263,8 @@ export class OnboardingService implements IOnboardingService {
|
|||
throw new BadRequestException('Please enter password');
|
||||
}
|
||||
|
||||
const activateDefaultWorkspace = (defaultWorkspace && defaultWorkspace.id === user.defaultOrganizationId) || allowPersonalWorkspace;
|
||||
const activateDefaultWorkspace =
|
||||
(defaultWorkspace && defaultWorkspace.id === user.defaultOrganizationId) || allowPersonalWorkspace;
|
||||
if (activateDefaultWorkspace) {
|
||||
// Getting default workspace
|
||||
const defaultOrganizationUser: OrganizationUser = user.organizationUsers.find(
|
||||
|
|
@ -277,11 +278,11 @@ export class OnboardingService implements IOnboardingService {
|
|||
// Activate default workspace
|
||||
await this.organizationUsersUtilService.activateOrganization(defaultOrganizationUser, manager);
|
||||
|
||||
if(defaultWorkspace && defaultWorkspace.id === user.defaultOrganizationId){
|
||||
if (defaultWorkspace && defaultWorkspace.id === user.defaultOrganizationId) {
|
||||
const personalWorkspaces = await this.organizationUsersUtilService.personalWorkspaces(user.id);
|
||||
for(const personalWorkspace of personalWorkspaces){
|
||||
for (const personalWorkspace of personalWorkspaces) {
|
||||
// if any personal workspace left. activate those
|
||||
await this.organizationUsersUtilService.activateOrganization(personalWorkspace, manager);
|
||||
await this.organizationUsersUtilService.activateOrganization(personalWorkspace, manager);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -362,6 +363,9 @@ export class OnboardingService implements IOnboardingService {
|
|||
organizationId: organization?.id,
|
||||
resourceId: user.id,
|
||||
resourceName: user.email,
|
||||
resourceData: {
|
||||
signup_method: 'self-signup',
|
||||
},
|
||||
});
|
||||
|
||||
await this.licenseUserService.validateUser(manager);
|
||||
|
|
@ -421,6 +425,13 @@ export class OnboardingService implements IOnboardingService {
|
|||
}
|
||||
const isWorkspaceSignup = organizationUser.source === WORKSPACE_USER_SOURCE.SIGNUP;
|
||||
await this.licenseUserService.validateUser(manager);
|
||||
const auditLogEntry = {
|
||||
userId: user.id,
|
||||
organizationId: organization.id,
|
||||
resourceId: user.id,
|
||||
resourceName: user.email,
|
||||
};
|
||||
RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, auditLogEntry);
|
||||
return this.sessionUtilService.generateLoginResultPayload(
|
||||
response,
|
||||
user,
|
||||
|
|
@ -534,6 +545,16 @@ export class OnboardingService implements IOnboardingService {
|
|||
Till now user doesn't have an organization.
|
||||
*/
|
||||
await this.licenseUserService.validateUser(manager);
|
||||
const auditLogsData = {
|
||||
userId: signupUser.id,
|
||||
organizationId: signupUser.organizationUsers[0].organizationId,
|
||||
resourceId: signupUser.id,
|
||||
resourceName: signupUser.email,
|
||||
resourceData: {
|
||||
signup_method: 'invite-redemption',
|
||||
},
|
||||
};
|
||||
RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, auditLogsData);
|
||||
return this.onboardingUtilService.processOrganizationSignup(
|
||||
response,
|
||||
signupUser,
|
||||
|
|
@ -566,7 +587,6 @@ export class OnboardingService implements IOnboardingService {
|
|||
if (user.status !== USER_STATUS.ACTIVE) {
|
||||
throw new BadRequestException(getUserErrorMessages(user.status));
|
||||
}
|
||||
|
||||
RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, {
|
||||
userId: user.id,
|
||||
organizationId: organizationUser.organizationId,
|
||||
|
|
|
|||
|
|
@ -6,12 +6,27 @@ export const FEATURES: FeaturesConfig = {
|
|||
[MODULES.ORGANIZATION_USER]: {
|
||||
[FEATURE_KEY.SUGGEST_USERS]: {},
|
||||
[FEATURE_KEY.VIEW_ALL_USERS]: {},
|
||||
[FEATURE_KEY.USER_ARCHIVE_ALL]: {},
|
||||
[FEATURE_KEY.USER_ARCHIVE]: {},
|
||||
[FEATURE_KEY.USER_INVITE]: {},
|
||||
[FEATURE_KEY.USER_ARCHIVE_ALL]: {
|
||||
isPublic: true,
|
||||
auditLogsKey: 'USER_ARCHIVE',
|
||||
},
|
||||
[FEATURE_KEY.USER_ARCHIVE]: {
|
||||
isPublic: true,
|
||||
auditLogsKey: 'USER_ARCHIVE',
|
||||
},
|
||||
[FEATURE_KEY.USER_INVITE]: {
|
||||
isPublic: true,
|
||||
auditLogsKey: 'USER_INVITE',
|
||||
},
|
||||
[FEATURE_KEY.USER_BULK_UPLOAD]: {},
|
||||
[FEATURE_KEY.USER_UNARCHIVE]: {},
|
||||
[FEATURE_KEY.USER_UNARCHIVE_ALL]: {},
|
||||
[FEATURE_KEY.USER_UNARCHIVE]: {
|
||||
isPublic: true,
|
||||
auditLogsKey: 'USER_UNARCHIVE',
|
||||
},
|
||||
[FEATURE_KEY.USER_UNARCHIVE_ALL]: {
|
||||
isPublic: true,
|
||||
auditLogsKey: 'USER_UNARCHIVE',
|
||||
},
|
||||
[FEATURE_KEY.USER_UPDATE]: {},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -90,14 +90,14 @@ export class OrganizationUsersController implements IOrganizationUsersController
|
|||
if (user.id === userId) {
|
||||
throw new NotAcceptableException('Self archive not allowed');
|
||||
}
|
||||
await this.organizationUsersService.archiveFromAll(userId);
|
||||
await this.organizationUsersService.archiveFromAll(userId, user);
|
||||
return;
|
||||
}
|
||||
|
||||
@InitFeature(FEATURE_KEY.USER_UNARCHIVE_ALL)
|
||||
@Post(':userId/unarchive-all')
|
||||
async unarchiveAll(@User() user: UserEntity, @Param('userId') userId: string) {
|
||||
await this.organizationUsersService.unarchiveUser(userId);
|
||||
await this.organizationUsersService.unarchiveUser(userId, user);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import { UpdateOrgUserDto } from '../dto';
|
|||
export interface IOrganizationUsersService {
|
||||
updateOrgUser(organizationUserId: string, user: User, updateOrgUserDto: UpdateOrgUserDto): Promise<void>;
|
||||
archive(id: string, organizationId: string, user?: User): Promise<void>;
|
||||
archiveFromAll(userId: string): Promise<void>;
|
||||
unarchiveUser(userId: string): Promise<void>;
|
||||
archiveFromAll(userId: string, user: User): Promise<void>;
|
||||
unarchiveUser(userId: string, user: User): Promise<void>;
|
||||
unarchive(user: User, id: string, organizationId: string): Promise<void>;
|
||||
inviteNewUser(currentUser: User, inviteNewUserDto: InviteNewUserDto): Promise<void>;
|
||||
bulkUploadUsers(currentUser: User, fileStream: any, res: Response): Promise<void>;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@ import { Response } from 'express';
|
|||
import { UserCsvRow } from './interfaces';
|
||||
import { IOrganizationUsersService } from './interfaces/IService';
|
||||
import { UpdateOrgUserDto } from './dto';
|
||||
import { RequestContext } from '@modules/request-context/service';
|
||||
import { AUDIT_LOGS_REQUEST_CONTEXT_KEY } from '@modules/app/constants';
|
||||
import { Organization } from '@entities/organization.entity';
|
||||
@Injectable()
|
||||
export class OrganizationUsersService implements IOrganizationUsersService {
|
||||
constructor(
|
||||
|
|
@ -38,7 +41,6 @@ export class OrganizationUsersService implements IOrganizationUsersService {
|
|||
|
||||
async updateOrgUser(organizationUserId: string, user: User, updateOrgUserDto: UpdateOrgUserDto) {
|
||||
const { firstName, lastName, addGroups, role, userMetadata } = updateOrgUserDto;
|
||||
|
||||
const organizationUser = await this.organizationUsersRepository.findOne({
|
||||
where: { id: organizationUserId, organizationId: user.organizationId },
|
||||
});
|
||||
|
|
@ -81,35 +83,84 @@ export class OrganizationUsersService implements IOrganizationUsersService {
|
|||
}
|
||||
|
||||
async archive(id: string, organizationId: string, user?: User): Promise<void> {
|
||||
const organizationUser = await this.organizationUsersRepository.findOneOrFail({
|
||||
where: { id, organizationId },
|
||||
relations: ['user'],
|
||||
});
|
||||
await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const organizationUser = await manager.findOneOrFail(OrganizationUser, {
|
||||
where: { id, organizationId },
|
||||
relations: ['user'],
|
||||
});
|
||||
|
||||
await this.organizationUsersUtilService.throwErrorIfUserIsLastActiveAdmin(organizationUser?.user, organizationId);
|
||||
await this.organizationUsersRepository.update(id, {
|
||||
status: WORKSPACE_USER_STATUS.ARCHIVED,
|
||||
invitationToken: null,
|
||||
await this.organizationUsersUtilService.throwErrorIfUserIsLastActiveAdmin(organizationUser?.user, organizationId);
|
||||
await manager.update(OrganizationUser, id, {
|
||||
status: WORKSPACE_USER_STATUS.ARCHIVED,
|
||||
invitationToken: null,
|
||||
});
|
||||
const organization = await manager.findOne(Organization, {
|
||||
where: { id: organizationUser.organizationId },
|
||||
});
|
||||
const auditLogEntry = {
|
||||
userId: user.id,
|
||||
organizationId: user.defaultOrganizationId,
|
||||
resourceId: user.id,
|
||||
resourceName: organizationUser.user.email,
|
||||
resourceData: {
|
||||
archived_user: {
|
||||
id: organizationUser.userId,
|
||||
email: organizationUser.user.email,
|
||||
first_name: organizationUser.user.firstName,
|
||||
last_name: organizationUser.user.lastName,
|
||||
},
|
||||
archived_user_workspace: {
|
||||
workspace_name: organization.name,
|
||||
workspace_id: organization.id,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, auditLogEntry);
|
||||
});
|
||||
}
|
||||
|
||||
async archiveFromAll(userId: string): Promise<void> {
|
||||
async archiveFromAll(userId: string, user: User): Promise<void> {
|
||||
await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const archivedUserWorkspaces = await manager.find(OrganizationUser, {
|
||||
where: { userId },
|
||||
relations: ['user'],
|
||||
});
|
||||
await manager.update(
|
||||
OrganizationUser,
|
||||
{ userId },
|
||||
{ status: WORKSPACE_USER_STATUS.ARCHIVED, invitationToken: null }
|
||||
);
|
||||
await this.organizationUsersUtilService.updateUserStatus(userId, USER_STATUS.ARCHIVED, manager);
|
||||
const organizationIds = archivedUserWorkspaces.map((user) => user.organizationId);
|
||||
const auditLogEntry = {
|
||||
userId: user.id,
|
||||
organizationIds: organizationIds,
|
||||
resourceId: user.id,
|
||||
resourceName: archivedUserWorkspaces[0].user.email,
|
||||
resourceData: {
|
||||
archived_user: {
|
||||
id: archivedUserWorkspaces[0].userId,
|
||||
email: archivedUserWorkspaces[0].user.email,
|
||||
first_name: archivedUserWorkspaces[0].user.firstName,
|
||||
last_name: archivedUserWorkspaces[0].user.lastName,
|
||||
},
|
||||
},
|
||||
};
|
||||
RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, auditLogEntry);
|
||||
});
|
||||
}
|
||||
|
||||
async unarchiveUser(userId: string): Promise<void> {
|
||||
async unarchiveUser(userId: string, user: User): Promise<void> {
|
||||
await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const targetUser = await manager.findOneOrFail(User, {
|
||||
where: { id: userId },
|
||||
select: ['id', 'status', 'invitationToken', 'source'],
|
||||
});
|
||||
const unarchivedUserWorkspaces = await manager.find(OrganizationUser, {
|
||||
where: { userId },
|
||||
relations: ['user'],
|
||||
});
|
||||
const { status, invitationToken } = targetUser;
|
||||
/* Special case. what if the user is archived when the status is invited. we were changing status to active before */
|
||||
const updatedStatus =
|
||||
|
|
@ -117,6 +168,22 @@ export class OrganizationUsersService implements IOrganizationUsersService {
|
|||
await this.organizationUsersUtilService.updateUserStatus(userId, updatedStatus, manager);
|
||||
await this.licenseUserService.validateUser(manager);
|
||||
await this.licenseOrganizationService.validateOrganization(manager);
|
||||
const organizationIds = unarchivedUserWorkspaces.map((user) => user.organizationId);
|
||||
const auditLogEntry = {
|
||||
userId: user.id,
|
||||
organizationIds: organizationIds,
|
||||
resourceId: user.id,
|
||||
resourceName: unarchivedUserWorkspaces[0].user.email,
|
||||
resourceData: {
|
||||
unarchived_user: {
|
||||
id: unarchivedUserWorkspaces[0].userId,
|
||||
email: unarchivedUserWorkspaces[0].user.email,
|
||||
first_name: unarchivedUserWorkspaces[0].user.firstName,
|
||||
last_name: unarchivedUserWorkspaces[0].user.lastName,
|
||||
},
|
||||
},
|
||||
};
|
||||
RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, auditLogEntry);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -144,6 +211,29 @@ export class OrganizationUsersService implements IOrganizationUsersService {
|
|||
|
||||
await this.licenseUserService.validateUser(manager);
|
||||
await this.licenseOrganizationService.validateOrganization(manager);
|
||||
const organization = await manager.findOne(Organization, {
|
||||
where: { id: organizationUser.organizationId },
|
||||
});
|
||||
const auditLogEntry = {
|
||||
userId: user.id,
|
||||
organizationId: user.defaultOrganizationId,
|
||||
resourceId: user.id,
|
||||
resourceName: organizationUser.user.email,
|
||||
resourceData: {
|
||||
unarchived_user: {
|
||||
id: organizationUser.userId,
|
||||
email: organizationUser.user.email,
|
||||
first_name: organizationUser.user.firstName,
|
||||
last_name: organizationUser.user.lastName,
|
||||
},
|
||||
unarchived_user_workspace: {
|
||||
workspace_name: organization.name,
|
||||
workspace_id: organization.id,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, auditLogEntry);
|
||||
});
|
||||
|
||||
if (organizationUser.user.invitationToken) {
|
||||
|
|
@ -160,6 +250,7 @@ export class OrganizationUsersService implements IOrganizationUsersService {
|
|||
sender: user.firstName,
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { User } from '@entities/user.entity';
|
||||
import { dbTransactionWrap } from '@helpers/database.helper';
|
||||
import { fullName, generateNextNameAndSlug } from '@helpers/utils.helper';
|
||||
import { EntityManager } from 'typeorm';
|
||||
import { EntityManager, In } from 'typeorm';
|
||||
import {
|
||||
getUserStatusAndSource,
|
||||
lifecycleEvents,
|
||||
|
|
@ -31,8 +31,6 @@ import { UserDetailsService } from './services/user-details.service';
|
|||
import { FetchUserResponse, InvitedUserType, RoleUpdate, UserFilterOptions } from './types';
|
||||
import { GroupPermissionsRepository } from '@modules/group-permissions/repository';
|
||||
import { ERROR_HANDLER, ERROR_HANDLER_TITLE } from '@modules/organizations/constants';
|
||||
import { MODULE_INFO } from '@modules/app/constants/module-info';
|
||||
import { MODULES } from '@modules/app/constants/modules';
|
||||
import { INSTANCE_USER_SETTINGS } from '@modules/instance-settings/constants';
|
||||
import { OrganizationRepository } from '@modules/organizations/repository';
|
||||
import * as uuid from 'uuid';
|
||||
|
|
@ -512,11 +510,33 @@ export class OrganizationUsersUtilService implements IOrganizationUsersUtilServi
|
|||
!user || !!user.invitationToken
|
||||
);
|
||||
|
||||
const groupsArray = [];
|
||||
if (inviteNewUserDto.groups && inviteNewUserDto.groups.length > 0) {
|
||||
const groupQuery = {
|
||||
organizationId: currentOrganization.id,
|
||||
id: In(inviteNewUserDto.groups),
|
||||
};
|
||||
const orgGroupPermissions = await this.groupPermissionsRepository.find({
|
||||
where: groupQuery,
|
||||
select: ['id', 'name'],
|
||||
});
|
||||
groupsArray.push(...orgGroupPermissions.map((group) => group.name));
|
||||
}
|
||||
RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, {
|
||||
userId: currentUser.id,
|
||||
organizationId: currentOrganization.id,
|
||||
resourceId: currentOrganization.id,
|
||||
resourceId: updatedUser.id,
|
||||
resourceName: updatedUser.email,
|
||||
resourceData: {
|
||||
invited_user: {
|
||||
id: updatedUser.id,
|
||||
email: updatedUser.email,
|
||||
first_name: updatedUser.firstName,
|
||||
last_name: updatedUser.lastName,
|
||||
role: inviteNewUserDto.role,
|
||||
group: groupsArray,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return organizationUser;
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue