Merge branch 'develop' into test/qa-18-api-automation-testing-permissionsuser-settings
This commit is contained in:
commit
457e0f8d82
|
@ -16,7 +16,8 @@
|
||||||
"dist",
|
"dist",
|
||||||
"public",
|
"public",
|
||||||
"*.spec.js",
|
"*.spec.js",
|
||||||
"bundle.js"
|
"bundle.js",
|
||||||
|
"packages/pro"
|
||||||
],
|
],
|
||||||
"plugins": ["svelte3"],
|
"plugins": ["svelte3"],
|
||||||
"extends": ["eslint:recommended"],
|
"extends": ["eslint:recommended"],
|
||||||
|
|
|
@ -1,43 +1,53 @@
|
||||||
name: Budibase Deploy Production
|
name: Budibase Deploy Production
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
version:
|
version:
|
||||||
description: Budibase release version. For example - 1.0.0
|
description: Budibase release version. For example - 1.0.0
|
||||||
required: false
|
required: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Fail if branch is not master
|
# - name: Fail if not a tag
|
||||||
if: github.ref != 'refs/heads/master'
|
# run: |
|
||||||
run: |
|
# if [[ $GITHUB_REF != refs/tags/* ]]; then
|
||||||
echo "Ref is not master, you must run this job from master."
|
# echo "Workflow Dispatch can only be run on tags"
|
||||||
exit 1
|
# exit 1
|
||||||
|
# fi
|
||||||
|
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
# with:
|
||||||
|
# fetch-depth: 0
|
||||||
|
|
||||||
|
# - name: Fail if tag is not in master
|
||||||
|
# run: |
|
||||||
|
# if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then
|
||||||
|
# echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
|
||||||
|
# exit 1
|
||||||
|
# fi
|
||||||
|
|
||||||
- name: Pull values.yaml from budibase-infra
|
- name: Pull values.yaml from budibase-infra
|
||||||
run: |
|
run: |
|
||||||
curl -H "Authorization: token ${{ secrets.GH_ACCESS_TOKEN }}" \
|
curl -H "Authorization: token ${{ secrets.GH_ACCESS_TOKEN }}" \
|
||||||
-H 'Accept: application/vnd.github.v3.raw' \
|
-H 'Accept: application/vnd.github.v3.raw' \
|
||||||
-o values.production.yaml \
|
-o values.production.yaml \
|
||||||
-L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/values.yaml
|
-L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/values.yaml
|
||||||
wc -l values.production.yaml
|
wc -l values.production.yaml
|
||||||
|
|
||||||
- name: Get the latest budibase release version
|
- name: Get the latest budibase release version
|
||||||
id: version
|
id: version
|
||||||
run: |
|
run: |
|
||||||
if [ -z "${{ github.event.inputs.version }}" ]; then
|
if [ -z "${{ github.event.inputs.version }}" ]; then
|
||||||
release_version=$(cat lerna.json | jq -r '.version')
|
release_version=$(cat lerna.json | jq -r '.version')
|
||||||
else
|
else
|
||||||
release_version=${{ github.event.inputs.version }}
|
release_version=${{ github.event.inputs.version }}
|
||||||
fi
|
fi
|
||||||
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Configure AWS Credentials
|
- name: Configure AWS Credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v1
|
uses: aws-actions/configure-aws-credentials@v1
|
||||||
with:
|
with:
|
||||||
|
@ -64,4 +74,3 @@ jobs:
|
||||||
webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }}
|
webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }}
|
||||||
content: "Production Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Cloud."
|
content: "Production Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Cloud."
|
||||||
embed-title: ${{ env.RELEASE_VERSION }}
|
embed-title: ${{ env.RELEASE_VERSION }}
|
||||||
|
|
||||||
|
|
|
@ -1,27 +1,35 @@
|
||||||
name: "deploy-preprod"
|
name: "deploy-preprod"
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
|
||||||
version:
|
|
||||||
description: Budibase release version. For example - 1.0.0
|
|
||||||
required: false
|
|
||||||
workflow_call:
|
workflow_call:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy-to-legacy-preprod-env:
|
deploy-to-legacy-preprod-env:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
- name: Fail if not a tag
|
||||||
|
run: |
|
||||||
|
if [[ $GITHUB_REF != refs/tags/* ]]; then
|
||||||
|
echo "Workflow Dispatch can only be run on tags"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Fail if tag is not in master
|
||||||
|
run: |
|
||||||
|
if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then
|
||||||
|
echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
- name: Get the latest budibase release version
|
- name: Get the latest budibase release version
|
||||||
id: version
|
id: version
|
||||||
run: |
|
run: |
|
||||||
if [ -z "${{ github.event.inputs.version }}" ]; then
|
release_version=$(cat lerna.json | jq -r '.version')
|
||||||
git pull
|
|
||||||
release_version=$(cat lerna.json | jq -r '.version')
|
|
||||||
else
|
|
||||||
release_version=${{ github.event.inputs.version }}
|
|
||||||
fi
|
|
||||||
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
||||||
- name: Configure AWS Credentials
|
- name: Configure AWS Credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v1
|
uses: aws-actions/configure-aws-credentials@v1
|
||||||
|
|
|
@ -22,6 +22,13 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Fail if not a tag
|
||||||
|
run: |
|
||||||
|
if [[ $GITHUB_REF != refs/tags/* ]]; then
|
||||||
|
echo "Workflow Dispatch can only be run on tags"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
@ -41,19 +48,8 @@ jobs:
|
||||||
|
|
||||||
- run: yarn install --frozen-lockfile
|
- run: yarn install --frozen-lockfile
|
||||||
- name: Update versions
|
- name: Update versions
|
||||||
run: |
|
run: ./scripts/updateVersions.sh
|
||||||
version=$(cat lerna.json \
|
- run: yarn build
|
||||||
| grep version \
|
|
||||||
| head -1 \
|
|
||||||
| awk -F: '{gsub(/"/,"",$2);gsub(/[[:space:]]*/,"",$2); print $2}' \
|
|
||||||
| sed 's/[",]//g')
|
|
||||||
echo "Setting version $version"
|
|
||||||
yarn lerna exec "yarn version --no-git-tag-version --new-version=$version"
|
|
||||||
echo "Updating dependencies"
|
|
||||||
node scripts/syncLocalDependencies.js $version
|
|
||||||
echo "Syncing yarn workspace"
|
|
||||||
yarn
|
|
||||||
- run: yarn build --configuration=production
|
|
||||||
- run: yarn build:sdk
|
- run: yarn build:sdk
|
||||||
|
|
||||||
- name: Publish budibase packages to NPM
|
- name: Publish budibase packages to NPM
|
||||||
|
|
|
@ -9,12 +9,6 @@ on:
|
||||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||||
# Exclude all pre-releases
|
# Exclude all pre-releases
|
||||||
- "!v*[0-9]+.[0-9]+.[0-9]+-*"
|
- "!v*[0-9]+.[0-9]+.[0-9]+-*"
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
tags:
|
|
||||||
description: "Release tag"
|
|
||||||
required: true
|
|
||||||
type: boolean
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
# Posthog token used by ui at build time
|
# Posthog token used by ui at build time
|
||||||
|
@ -33,12 +27,13 @@ jobs:
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Fail if branch is not master
|
- name: Fail if tag is not in master
|
||||||
if: github.ref != 'refs/heads/master'
|
|
||||||
run: |
|
run: |
|
||||||
echo "Ref is not master, you must run this job from master."
|
if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then
|
||||||
// Change to "exit 1" when merged. Left to 0 to not fail all the pipelines and not to cause noise
|
echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
|
||||||
exit 0
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v1
|
- uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
|
@ -46,26 +41,14 @@ jobs:
|
||||||
|
|
||||||
- run: yarn install --frozen-lockfile
|
- run: yarn install --frozen-lockfile
|
||||||
- name: Update versions
|
- name: Update versions
|
||||||
run: |
|
run: ./scripts/updateVersions.sh
|
||||||
version=$(cat lerna.json \
|
|
||||||
| grep version \
|
|
||||||
| head -1 \
|
|
||||||
| awk -F: '{gsub(/"/,"",$2);gsub(/[[:space:]]*/,"",$2); print $2}' \
|
|
||||||
| sed 's/[",]//g')
|
|
||||||
echo "Setting version $version"
|
|
||||||
yarn lerna exec "yarn version --no-git-tag-version --new-version=$version"
|
|
||||||
echo "Updating dependencies"
|
|
||||||
node scripts/syncLocalDependencies.js $version
|
|
||||||
echo "Syncing yarn workspace"
|
|
||||||
yarn
|
|
||||||
- run: yarn lint
|
- run: yarn lint
|
||||||
- run: yarn build --configuration=production
|
- run: yarn build
|
||||||
- run: yarn build:sdk
|
- run: yarn build:sdk
|
||||||
|
|
||||||
- name: Publish budibase packages to NPM
|
- name: Publish budibase packages to NPM
|
||||||
env:
|
env:
|
||||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
RELEASE_VERSION_TYPE: ${{ github.event.inputs.versioning }}
|
|
||||||
run: |
|
run: |
|
||||||
# setup the username and email. I tend to use 'GitHub Actions Bot' with no email by default
|
# setup the username and email. I tend to use 'GitHub Actions Bot' with no email by default
|
||||||
git config --global user.name "Budibase Release Bot"
|
git config --global user.name "Budibase Release Bot"
|
||||||
|
@ -140,7 +123,6 @@ jobs:
|
||||||
- name: Get the latest budibase release version
|
- name: Get the latest budibase release version
|
||||||
id: version
|
id: version
|
||||||
run: |
|
run: |
|
||||||
git pull
|
|
||||||
release_version=$(cat lerna.json | jq -r '.version')
|
release_version=$(cat lerna.json | jq -r '.version')
|
||||||
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,32 @@
|
||||||
name: Budibase Release Selfhost
|
name: Budibase Release Selfhost
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Fail if branch is not master
|
- name: Fail if not a tag
|
||||||
if: github.ref != 'refs/heads/master'
|
|
||||||
run: |
|
run: |
|
||||||
echo "Ref is not master, you must run this job from master."
|
if [[ $GITHUB_REF != refs/tags/* ]]; then
|
||||||
exit 1
|
echo "Workflow Dispatch can only be run on tags"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
fetch_depth: 0
|
submodules: true
|
||||||
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Fail if tag is not in master
|
||||||
|
run: |
|
||||||
|
if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then
|
||||||
|
echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Use Node.js 14.x
|
- name: Use Node.js 14.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
|
@ -30,7 +40,7 @@ jobs:
|
||||||
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Tag and release Docker images (Self Host)
|
- name: Tag and release Docker images (Self Host)
|
||||||
run: |
|
run: |
|
||||||
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
|
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
|
||||||
|
|
||||||
release_tag=v${{ env.RELEASE_VERSION }}
|
release_tag=v${{ env.RELEASE_VERSION }}
|
||||||
|
@ -44,7 +54,7 @@ jobs:
|
||||||
docker tag budibase/apps:$release_tag budibase/apps:$SELFHOST_TAG
|
docker tag budibase/apps:$release_tag budibase/apps:$SELFHOST_TAG
|
||||||
docker tag budibase/worker:$release_tag budibase/worker:$SELFHOST_TAG
|
docker tag budibase/worker:$release_tag budibase/worker:$SELFHOST_TAG
|
||||||
docker tag budibase/proxy:$release_tag budibase/proxy:$SELFHOST_TAG
|
docker tag budibase/proxy:$release_tag budibase/proxy:$SELFHOST_TAG
|
||||||
|
|
||||||
# Push images
|
# Push images
|
||||||
docker push budibase/apps:$SELFHOST_TAG
|
docker push budibase/apps:$SELFHOST_TAG
|
||||||
docker push budibase/worker:$SELFHOST_TAG
|
docker push budibase/worker:$SELFHOST_TAG
|
||||||
|
@ -66,19 +76,19 @@ jobs:
|
||||||
yarn
|
yarn
|
||||||
yarn specs
|
yarn specs
|
||||||
popd
|
popd
|
||||||
|
|
||||||
- name: Setup Helm
|
- name: Setup Helm
|
||||||
uses: azure/setup-helm@v1
|
uses: azure/setup-helm@v1
|
||||||
id: helm-install
|
id: helm-install
|
||||||
|
|
||||||
# due to helm repo index issue: https://github.com/helm/helm/issues/7363
|
# due to helm repo index issue: https://github.com/helm/helm/issues/7363
|
||||||
# we need to create new package in a different dir, merge the index and move the package back
|
# we need to create new package in a different dir, merge the index and move the package back
|
||||||
- name: Build and release helm chart
|
- name: Build and release helm chart
|
||||||
run: |
|
run: |
|
||||||
git config user.name "Budibase Helm Bot"
|
git config user.name "Budibase Helm Bot"
|
||||||
git config user.email "<>"
|
git config user.email "<>"
|
||||||
git reset --hard
|
git reset --hard
|
||||||
git pull
|
git fetch
|
||||||
mkdir sync
|
mkdir sync
|
||||||
echo "Packaging chart to sync dir"
|
echo "Packaging chart to sync dir"
|
||||||
helm package charts/budibase --version "$RELEASE_VERSION" --app-version "$RELEASE_VERSION" --destination sync
|
helm package charts/budibase --version "$RELEASE_VERSION" --app-version "$RELEASE_VERSION" --destination sync
|
||||||
|
|
|
@ -5,7 +5,7 @@ on:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CI: true
|
CI: true
|
||||||
PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
REGISTRY_URL: registry.hub.docker.com
|
REGISTRY_URL: registry.hub.docker.com
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
@ -15,13 +15,26 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [14.x]
|
node-version: [14.x]
|
||||||
steps:
|
steps:
|
||||||
- name: Fail if branch is not master
|
- name: Fail if not a tag
|
||||||
if: github.ref != 'refs/heads/master'
|
run: |
|
||||||
run: |
|
if [[ $GITHUB_REF != refs/tags/* ]]; then
|
||||||
echo "Ref is not master, you must run this job from master."
|
echo "Workflow Dispatch can only be run on tags"
|
||||||
exit 1
|
exit 1
|
||||||
|
fi
|
||||||
- name: "Checkout"
|
- name: "Checkout"
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Fail if tag is not in master
|
||||||
|
run: |
|
||||||
|
if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then
|
||||||
|
echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
|
@ -33,10 +46,12 @@ jobs:
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v1
|
||||||
- name: Run Yarn
|
- name: Run Yarn
|
||||||
run: yarn
|
run: yarn
|
||||||
- name: Run Yarn Bootstrap
|
- name: Update versions
|
||||||
run: yarn bootstrap
|
run: ./scripts/updateVersions.sh
|
||||||
- name: Runt Yarn Lint
|
- name: Runt Yarn Lint
|
||||||
run: yarn lint
|
run: yarn lint
|
||||||
|
- name: Update versions
|
||||||
|
run: ./scripts/updateVersions.sh
|
||||||
- name: Run Yarn Build
|
- name: Run Yarn Build
|
||||||
run: yarn build:docker:pre
|
run: yarn build:docker:pre
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
|
|
|
@ -28,7 +28,7 @@ on:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tag-prerelease:
|
tag-release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
@ -43,9 +43,11 @@ jobs:
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
|
|
||||||
- run: yarn
|
- run: yarn
|
||||||
- name: Tag prerelease
|
- name: Tag release
|
||||||
run: |
|
run: |
|
||||||
# setup the username and email.
|
# setup the username and email.
|
||||||
git config --global user.name "Budibase Staging Release Bot"
|
git config --global user.name "Budibase Staging Release Bot"
|
||||||
git config --global user.email "<>"
|
git config --global user.email "<>"
|
||||||
./scripts/versionCommit.sh ${{ github.event.inputs.versioning }}
|
BUMP_TYPE_INPUT=${{ github.event.inputs.versioning }}
|
||||||
|
BUMP_TYPE=${BUMP_TYPE_INPUT:-"patch"}
|
||||||
|
./scripts/versionCommit.sh $BUMP_TYPE
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM node:16-slim as build
|
FROM node:14-slim as build
|
||||||
|
|
||||||
# install node-gyp dependencies
|
# install node-gyp dependencies
|
||||||
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends apt-utils cron g++ make python
|
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends apt-utils cron g++ make python
|
||||||
|
@ -11,12 +11,16 @@ RUN chmod +x /cleanup.sh
|
||||||
# build server
|
# build server
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ADD packages/server .
|
ADD packages/server .
|
||||||
RUN yarn install --frozen-lockfile --production=true && /cleanup.sh
|
COPY yarn.lock .
|
||||||
|
RUN yarn install --production=true
|
||||||
|
RUN /cleanup.sh
|
||||||
|
|
||||||
# build worker
|
# build worker
|
||||||
WORKDIR /worker
|
WORKDIR /worker
|
||||||
ADD packages/worker .
|
ADD packages/worker .
|
||||||
RUN yarn install --frozen-lockfile --production=true && /cleanup.sh
|
COPY yarn.lock .
|
||||||
|
RUN yarn install --production=true
|
||||||
|
RUN /cleanup.sh
|
||||||
|
|
||||||
FROM budibase/couchdb
|
FROM budibase/couchdb
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
|
|
|
@ -17,6 +17,7 @@ declare -a DOCKER_VARS=("APP_PORT" "APPS_URL" "ARCHITECTURE" "BUDIBASE_ENVIRONME
|
||||||
[[ -z "${WORKER_PORT}" ]] && export WORKER_PORT=4002
|
[[ -z "${WORKER_PORT}" ]] && export WORKER_PORT=4002
|
||||||
[[ -z "${WORKER_URL}" ]] && export WORKER_URL=http://localhost:4002
|
[[ -z "${WORKER_URL}" ]] && export WORKER_URL=http://localhost:4002
|
||||||
[[ -z "${APPS_URL}" ]] && export APPS_URL=http://localhost:4001
|
[[ -z "${APPS_URL}" ]] && export APPS_URL=http://localhost:4001
|
||||||
|
[[ -z "${SERVER_TOP_LEVEL_PATH}" ]] && export SERVER_TOP_LEVEL_PATH=/app
|
||||||
# export CUSTOM_DOMAIN=budi001.custom.com
|
# export CUSTOM_DOMAIN=budi001.custom.com
|
||||||
|
|
||||||
# Azure App Service customisations
|
# Azure App Service customisations
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.6.19-alpha.54",
|
"version": "2.7.7-alpha.2",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/backend-core",
|
"packages/backend-core",
|
||||||
|
|
|
@ -38,8 +38,8 @@
|
||||||
"backend:build": "./scripts/scopeBackend.sh 'lerna run --stream build'",
|
"backend:build": "./scripts/scopeBackend.sh 'lerna run --stream build'",
|
||||||
"build:sdk": "lerna run --stream build:sdk",
|
"build:sdk": "lerna run --stream build:sdk",
|
||||||
"deps:circular": "madge packages/server/dist/index.js packages/worker/src/index.ts packages/backend-core/dist/src/index.js packages/cli/src/index.js --circular",
|
"deps:circular": "madge packages/server/dist/index.js packages/worker/src/index.ts packages/backend-core/dist/src/index.js packages/cli/src/index.js --circular",
|
||||||
"release": "lerna publish ${RELEASE_VERSION_TYPE:-patch} --yes --force-publish --no-git-tag-version --no-push --no-git-reset",
|
"release": "lerna publish from-package --yes --force-publish --no-git-tag-version --no-push --no-git-reset",
|
||||||
"release:develop": "lerna publish from-package --yes --force-publish --dist-tag develop --exact --no-git-tag-version --no-push --no-git-reset",
|
"release:develop": "yarn release --dist-tag develop",
|
||||||
"restore": "yarn run clean && yarn run bootstrap && yarn run build",
|
"restore": "yarn run clean && yarn run bootstrap && yarn run build",
|
||||||
"nuke": "yarn run nuke:packages && yarn run nuke:docker",
|
"nuke": "yarn run nuke:packages && yarn run nuke:docker",
|
||||||
"nuke:packages": "yarn run restore",
|
"nuke:packages": "yarn run restore",
|
||||||
|
@ -52,7 +52,7 @@
|
||||||
"dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up && lerna run --stream --parallel dev:builder --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
|
"dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up && lerna run --stream --parallel dev:builder --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
|
||||||
"dev:server": "yarn run kill-server && lerna run --stream --parallel dev:builder --scope @budibase/worker --scope @budibase/server",
|
"dev:server": "yarn run kill-server && lerna run --stream --parallel dev:builder --scope @budibase/worker --scope @budibase/server",
|
||||||
"dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream --parallel dev:built",
|
"dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream --parallel dev:built",
|
||||||
"dev:docker": "yarn build && docker-compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0",
|
"dev:docker": "yarn build:docker:pre && docker-compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0",
|
||||||
"test": "lerna run --stream test --stream",
|
"test": "lerna run --stream test --stream",
|
||||||
"lint:eslint": "eslint packages && eslint qa-core",
|
"lint:eslint": "eslint packages && eslint qa-core",
|
||||||
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --check \"qa-core/**/*.{js,ts,svelte}\"",
|
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --check \"qa-core/**/*.{js,ts,svelte}\"",
|
||||||
|
@ -62,7 +62,7 @@
|
||||||
"lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint",
|
"lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint",
|
||||||
"build:specs": "lerna run --stream specs",
|
"build:specs": "lerna run --stream specs",
|
||||||
"build:docker": "lerna run --stream build:docker && yarn build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -",
|
"build:docker": "lerna run --stream build:docker && yarn build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -",
|
||||||
"build:docker:pre": "lerna run --stream build && lerna run --stream predocker",
|
"build:docker:pre": "yarn build && lerna run --stream predocker",
|
||||||
"build:docker:proxy": "docker build hosting/proxy -t proxy-service",
|
"build:docker:proxy": "docker build hosting/proxy -t proxy-service",
|
||||||
"build:docker:selfhost": "lerna run --stream build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh latest && cd -",
|
"build:docker:selfhost": "lerna run --stream build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh latest && cd -",
|
||||||
"build:docker:develop": "node scripts/pinVersions && lerna run --stream build:docker && yarn build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
|
"build:docker:develop": "node scripts/pinVersions && lerna run --stream build:docker && yarn build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
|
||||||
|
|
|
@ -86,6 +86,7 @@ const getCurrentIdentity = async (): Promise<Identity> => {
|
||||||
installationId,
|
installationId,
|
||||||
tenantId,
|
tenantId,
|
||||||
environment,
|
environment,
|
||||||
|
realTenantId: context.getTenantId(),
|
||||||
hostInfo: userContext.hostInfo,
|
hostInfo: userContext.hostInfo,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -3,6 +3,7 @@ import {
|
||||||
Event,
|
Event,
|
||||||
User,
|
User,
|
||||||
UserCreatedEvent,
|
UserCreatedEvent,
|
||||||
|
UserDataCollaborationEvent,
|
||||||
UserDeletedEvent,
|
UserDeletedEvent,
|
||||||
UserInviteAcceptedEvent,
|
UserInviteAcceptedEvent,
|
||||||
UserInvitedEvent,
|
UserInvitedEvent,
|
||||||
|
@ -173,6 +174,15 @@ async function passwordReset(user: User) {
|
||||||
await publishEvent(Event.USER_PASSWORD_RESET, properties)
|
await publishEvent(Event.USER_PASSWORD_RESET, properties)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// COLLABORATION
|
||||||
|
|
||||||
|
async function dataCollaboration(users: number) {
|
||||||
|
const properties: UserDataCollaborationEvent = {
|
||||||
|
users,
|
||||||
|
}
|
||||||
|
await publishEvent(Event.USER_DATA_COLLABORATION, properties)
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
created,
|
created,
|
||||||
updated,
|
updated,
|
||||||
|
@ -188,4 +198,5 @@ export default {
|
||||||
passwordUpdated,
|
passwordUpdated,
|
||||||
passwordResetRequested,
|
passwordResetRequested,
|
||||||
passwordReset,
|
passwordReset,
|
||||||
|
dataCollaboration,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import * as google from "../sso/google"
|
import * as google from "../sso/google"
|
||||||
import { Cookie } from "../../../constants"
|
import { Cookie } from "../../../constants"
|
||||||
import { clearCookie, getCookie } from "../../../utils"
|
|
||||||
import { doWithDB } from "../../../db"
|
|
||||||
import * as configs from "../../../configs"
|
import * as configs from "../../../configs"
|
||||||
import { BBContext, Database, SSOProfile } from "@budibase/types"
|
import * as cache from "../../../cache"
|
||||||
|
import * as utils from "../../../utils"
|
||||||
|
import { UserCtx, SSOProfile } from "@budibase/types"
|
||||||
import { ssoSaveUserNoOp } from "../sso/sso"
|
import { ssoSaveUserNoOp } from "../sso/sso"
|
||||||
|
|
||||||
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
|
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
|
||||||
|
|
||||||
type Passport = {
|
type Passport = {
|
||||||
|
@ -22,7 +23,7 @@ async function fetchGoogleCreds() {
|
||||||
|
|
||||||
export async function preAuth(
|
export async function preAuth(
|
||||||
passport: Passport,
|
passport: Passport,
|
||||||
ctx: BBContext,
|
ctx: UserCtx,
|
||||||
next: Function
|
next: Function
|
||||||
) {
|
) {
|
||||||
// get the relevant config
|
// get the relevant config
|
||||||
|
@ -36,8 +37,8 @@ export async function preAuth(
|
||||||
ssoSaveUserNoOp
|
ssoSaveUserNoOp
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!ctx.query.appId || !ctx.query.datasourceId) {
|
if (!ctx.query.appId) {
|
||||||
ctx.throw(400, "appId and datasourceId query params not present.")
|
ctx.throw(400, "appId query param not present.")
|
||||||
}
|
}
|
||||||
|
|
||||||
return passport.authenticate(strategy, {
|
return passport.authenticate(strategy, {
|
||||||
|
@ -49,7 +50,7 @@ export async function preAuth(
|
||||||
|
|
||||||
export async function postAuth(
|
export async function postAuth(
|
||||||
passport: Passport,
|
passport: Passport,
|
||||||
ctx: BBContext,
|
ctx: UserCtx,
|
||||||
next: Function
|
next: Function
|
||||||
) {
|
) {
|
||||||
// get the relevant config
|
// get the relevant config
|
||||||
|
@ -57,7 +58,7 @@ export async function postAuth(
|
||||||
const platformUrl = await configs.getPlatformUrl({ tenantAware: false })
|
const platformUrl = await configs.getPlatformUrl({ tenantAware: false })
|
||||||
|
|
||||||
let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback`
|
let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback`
|
||||||
const authStateCookie = getCookie(ctx, Cookie.DatasourceAuth)
|
const authStateCookie = utils.getCookie(ctx, Cookie.DatasourceAuth)
|
||||||
|
|
||||||
return passport.authenticate(
|
return passport.authenticate(
|
||||||
new GoogleStrategy(
|
new GoogleStrategy(
|
||||||
|
@ -69,33 +70,26 @@ export async function postAuth(
|
||||||
(
|
(
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
refreshToken: string,
|
refreshToken: string,
|
||||||
profile: SSOProfile,
|
_profile: SSOProfile,
|
||||||
done: Function
|
done: Function
|
||||||
) => {
|
) => {
|
||||||
clearCookie(ctx, Cookie.DatasourceAuth)
|
utils.clearCookie(ctx, Cookie.DatasourceAuth)
|
||||||
done(null, { accessToken, refreshToken })
|
done(null, { accessToken, refreshToken })
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
{ successRedirect: "/", failureRedirect: "/error" },
|
{ successRedirect: "/", failureRedirect: "/error" },
|
||||||
async (err: any, tokens: string[]) => {
|
async (err: any, tokens: string[]) => {
|
||||||
const baseUrl = `/builder/app/${authStateCookie.appId}/data`
|
const baseUrl = `/builder/app/${authStateCookie.appId}/data`
|
||||||
// update the DB for the datasource with all the user info
|
|
||||||
await doWithDB(authStateCookie.appId, async (db: Database) => {
|
const id = utils.newid()
|
||||||
let datasource
|
await cache.store(
|
||||||
try {
|
`datasource:creation:${authStateCookie.appId}:google:${id}`,
|
||||||
datasource = await db.get(authStateCookie.datasourceId)
|
{
|
||||||
} catch (err: any) {
|
tokens,
|
||||||
if (err.status === 404) {
|
|
||||||
ctx.redirect(baseUrl)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!datasource.config) {
|
)
|
||||||
datasource.config = {}
|
|
||||||
}
|
ctx.redirect(`${baseUrl}/new?continue_google_setup=${id}`)
|
||||||
datasource.config.auth = { type: "google", ...tokens }
|
|
||||||
await db.put(datasource)
|
|
||||||
ctx.redirect(`${baseUrl}/datasource/${authStateCookie.datasourceId}`)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
)(ctx, next)
|
)(ctx, next)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,8 @@
|
||||||
Modal,
|
Modal,
|
||||||
notifications,
|
notifications,
|
||||||
Icon,
|
Icon,
|
||||||
|
Checkbox,
|
||||||
|
DatePicker,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||||
import { automationStore, selectedAutomation } from "builderStore"
|
import { automationStore, selectedAutomation } from "builderStore"
|
||||||
|
@ -306,6 +308,11 @@
|
||||||
drawer.hide()
|
drawer.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function canShowField(key, value) {
|
||||||
|
const dependsOn = value.dependsOn
|
||||||
|
return !dependsOn || !!inputData[dependsOn]
|
||||||
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
await environment.loadVariables()
|
await environment.loadVariables()
|
||||||
|
@ -317,210 +324,233 @@
|
||||||
|
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
{#each deprecatedSchemaProperties as [key, value]}
|
{#each deprecatedSchemaProperties as [key, value]}
|
||||||
<div class="block-field">
|
{#if canShowField(key, value)}
|
||||||
{#if key !== "fields"}
|
<div class="block-field">
|
||||||
<Label
|
{#if key !== "fields" && value.type !== "boolean"}
|
||||||
tooltip={value.title === "Binding / Value"
|
<Label
|
||||||
? "If using the String input type, please use a comma or newline separated string"
|
tooltip={value.title === "Binding / Value"
|
||||||
: null}>{value.title || (key === "row" ? "Table" : key)}</Label
|
? "If using the String input type, please use a comma or newline separated string"
|
||||||
>
|
: null}>{value.title || (key === "row" ? "Table" : key)}</Label
|
||||||
{/if}
|
>
|
||||||
{#if value.type === "string" && value.enum}
|
|
||||||
<Select
|
|
||||||
on:change={e => onChange(e, key)}
|
|
||||||
value={inputData[key]}
|
|
||||||
placeholder={false}
|
|
||||||
options={value.enum}
|
|
||||||
getOptionLabel={(x, idx) => (value.pretty ? value.pretty[idx] : x)}
|
|
||||||
/>
|
|
||||||
{:else if value.type === "json"}
|
|
||||||
<Editor
|
|
||||||
editorHeight="250"
|
|
||||||
editorWidth="448"
|
|
||||||
mode="json"
|
|
||||||
value={inputData[key]?.value}
|
|
||||||
on:change={e => {
|
|
||||||
/**
|
|
||||||
* TODO - Remove after November 2023
|
|
||||||
* *******************************
|
|
||||||
* Code added to provide backwards compatibility between Values 1,2,3,4,5
|
|
||||||
* and the new JSON body.
|
|
||||||
*/
|
|
||||||
delete inputData.value1
|
|
||||||
delete inputData.value2
|
|
||||||
delete inputData.value3
|
|
||||||
delete inputData.value4
|
|
||||||
delete inputData.value5
|
|
||||||
/***********************/
|
|
||||||
onChange(e, key)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{:else if value.customType === "column"}
|
|
||||||
<Select
|
|
||||||
on:change={e => onChange(e, key)}
|
|
||||||
value={inputData[key]}
|
|
||||||
options={Object.keys(table?.schema || {})}
|
|
||||||
/>
|
|
||||||
{:else if value.customType === "filters"}
|
|
||||||
<ActionButton on:click={drawer.show}>Define filters</ActionButton>
|
|
||||||
<Drawer bind:this={drawer} {fillWidth} title="Filtering">
|
|
||||||
<Button cta slot="buttons" on:click={() => saveFilters(key)}>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
<FilterDrawer
|
|
||||||
slot="body"
|
|
||||||
{filters}
|
|
||||||
{bindings}
|
|
||||||
{schemaFields}
|
|
||||||
datasource={{ type: "table", tableId }}
|
|
||||||
panel={AutomationBindingPanel}
|
|
||||||
fillWidth
|
|
||||||
on:change={e => (tempFilters = e.detail)}
|
|
||||||
/>
|
|
||||||
</Drawer>
|
|
||||||
{:else if value.customType === "password"}
|
|
||||||
<Input
|
|
||||||
type="password"
|
|
||||||
on:change={e => onChange(e, key)}
|
|
||||||
value={inputData[key]}
|
|
||||||
/>
|
|
||||||
{:else if value.customType === "email"}
|
|
||||||
{#if isTestModal}
|
|
||||||
<ModalBindableInput
|
|
||||||
title={value.title}
|
|
||||||
value={inputData[key]}
|
|
||||||
panel={AutomationBindingPanel}
|
|
||||||
type="email"
|
|
||||||
on:change={e => onChange(e, key)}
|
|
||||||
{bindings}
|
|
||||||
fillWidth
|
|
||||||
updateOnChange={false}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<DrawerBindableInput
|
|
||||||
fillWidth
|
|
||||||
title={value.title}
|
|
||||||
panel={AutomationBindingPanel}
|
|
||||||
type="email"
|
|
||||||
value={inputData[key]}
|
|
||||||
on:change={e => onChange(e, key)}
|
|
||||||
{bindings}
|
|
||||||
allowJS={false}
|
|
||||||
updateOnChange={false}
|
|
||||||
drawerLeft="260px"
|
|
||||||
/>
|
|
||||||
{/if}
|
{/if}
|
||||||
{:else if value.customType === "query"}
|
{#if value.type === "string" && value.enum && canShowField(key)}
|
||||||
<QuerySelector
|
<Select
|
||||||
on:change={e => onChange(e, key)}
|
|
||||||
value={inputData[key]}
|
|
||||||
/>
|
|
||||||
{:else if value.customType === "cron"}
|
|
||||||
<CronBuilder on:change={e => onChange(e, key)} value={inputData[key]} />
|
|
||||||
{:else if value.customType === "queryParams"}
|
|
||||||
<QueryParamSelector
|
|
||||||
on:change={e => onChange(e, key)}
|
|
||||||
value={inputData[key]}
|
|
||||||
{bindings}
|
|
||||||
/>
|
|
||||||
{:else if value.customType === "table"}
|
|
||||||
<TableSelector
|
|
||||||
{isTrigger}
|
|
||||||
value={inputData[key]}
|
|
||||||
on:change={e => onChange(e, key)}
|
|
||||||
/>
|
|
||||||
{:else if value.customType === "row"}
|
|
||||||
<RowSelector
|
|
||||||
{block}
|
|
||||||
value={inputData[key]}
|
|
||||||
meta={inputData["meta"] || {}}
|
|
||||||
on:change={e => {
|
|
||||||
if (e.detail?.key) {
|
|
||||||
onChange(e, e.detail.key)
|
|
||||||
} else {
|
|
||||||
onChange(e, key)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
{bindings}
|
|
||||||
{isTestModal}
|
|
||||||
{isUpdateRow}
|
|
||||||
/>
|
|
||||||
{:else if value.customType === "webhookUrl"}
|
|
||||||
<WebhookDisplay
|
|
||||||
on:change={e => onChange(e, key)}
|
|
||||||
value={inputData[key]}
|
|
||||||
/>
|
|
||||||
{:else if value.customType === "fields"}
|
|
||||||
<FieldSelector
|
|
||||||
{block}
|
|
||||||
value={inputData[key]}
|
|
||||||
on:change={e => onChange(e, key)}
|
|
||||||
{bindings}
|
|
||||||
{isTestModal}
|
|
||||||
/>
|
|
||||||
{:else if value.customType === "triggerSchema"}
|
|
||||||
<SchemaSetup on:change={e => onChange(e, key)} value={inputData[key]} />
|
|
||||||
{:else if value.customType === "code"}
|
|
||||||
<CodeEditorModal>
|
|
||||||
<CodeEditor
|
|
||||||
value={inputData[key]}
|
|
||||||
on:change={e => {
|
|
||||||
// need to pass without the value inside
|
|
||||||
onChange({ detail: e.detail }, key)
|
|
||||||
inputData[key] = e.detail
|
|
||||||
}}
|
|
||||||
completions={[
|
|
||||||
jsAutocomplete([
|
|
||||||
...bindingsToCompletions(bindings, EditorModes.JS),
|
|
||||||
]),
|
|
||||||
]}
|
|
||||||
mode={EditorModes.JS}
|
|
||||||
height={500}
|
|
||||||
/>
|
|
||||||
<div class="messaging">
|
|
||||||
<Icon name="FlashOn" />
|
|
||||||
<div class="messaging-wrap">
|
|
||||||
<div>Add available bindings by typing <strong>$</strong></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CodeEditorModal>
|
|
||||||
{:else if value.customType === "loopOption"}
|
|
||||||
<Select
|
|
||||||
on:change={e => onChange(e, key)}
|
|
||||||
autoWidth
|
|
||||||
value={inputData[key]}
|
|
||||||
options={["Array", "String"]}
|
|
||||||
defaultValue={"Array"}
|
|
||||||
/>
|
|
||||||
{:else if value.type === "string" || value.type === "number" || value.type === "integer"}
|
|
||||||
{#if isTestModal}
|
|
||||||
<ModalBindableInput
|
|
||||||
title={value.title}
|
|
||||||
value={inputData[key]}
|
|
||||||
panel={AutomationBindingPanel}
|
|
||||||
type={value.customType}
|
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange(e, key)}
|
||||||
{bindings}
|
value={inputData[key]}
|
||||||
updateOnChange={false}
|
placeholder={false}
|
||||||
|
options={value.enum}
|
||||||
|
getOptionLabel={(x, idx) => (value.pretty ? value.pretty[idx] : x)}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else if value.type === "json"}
|
||||||
<div class="test">
|
<Editor
|
||||||
|
editorHeight="250"
|
||||||
|
editorWidth="448"
|
||||||
|
mode="json"
|
||||||
|
value={inputData[key]?.value}
|
||||||
|
on:change={e => {
|
||||||
|
/**
|
||||||
|
* TODO - Remove after November 2023
|
||||||
|
* *******************************
|
||||||
|
* Code added to provide backwards compatibility between Values 1,2,3,4,5
|
||||||
|
* and the new JSON body.
|
||||||
|
*/
|
||||||
|
delete inputData.value1
|
||||||
|
delete inputData.value2
|
||||||
|
delete inputData.value3
|
||||||
|
delete inputData.value4
|
||||||
|
delete inputData.value5
|
||||||
|
/***********************/
|
||||||
|
onChange(e, key)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{:else if value.type === "boolean"}
|
||||||
|
<div style="margin-top: 10px">
|
||||||
|
<Checkbox
|
||||||
|
text={value.title}
|
||||||
|
value={inputData[key]}
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{:else if value.type === "date"}
|
||||||
|
<DatePicker
|
||||||
|
value={inputData[key]}
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
/>
|
||||||
|
{:else if value.customType === "column"}
|
||||||
|
<Select
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
value={inputData[key]}
|
||||||
|
options={Object.keys(table?.schema || {})}
|
||||||
|
/>
|
||||||
|
{:else if value.customType === "filters"}
|
||||||
|
<ActionButton on:click={drawer.show}>Define filters</ActionButton>
|
||||||
|
<Drawer bind:this={drawer} {fillWidth} title="Filtering">
|
||||||
|
<Button cta slot="buttons" on:click={() => saveFilters(key)}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<FilterDrawer
|
||||||
|
slot="body"
|
||||||
|
{filters}
|
||||||
|
{bindings}
|
||||||
|
{schemaFields}
|
||||||
|
datasource={{ type: "table", tableId }}
|
||||||
|
panel={AutomationBindingPanel}
|
||||||
|
fillWidth
|
||||||
|
on:change={e => (tempFilters = e.detail)}
|
||||||
|
/>
|
||||||
|
</Drawer>
|
||||||
|
{:else if value.customType === "password"}
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
value={inputData[key]}
|
||||||
|
/>
|
||||||
|
{:else if value.customType === "email"}
|
||||||
|
{#if isTestModal}
|
||||||
|
<ModalBindableInput
|
||||||
|
title={value.title}
|
||||||
|
value={inputData[key]}
|
||||||
|
panel={AutomationBindingPanel}
|
||||||
|
type="email"
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
{bindings}
|
||||||
|
fillWidth
|
||||||
|
updateOnChange={false}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
<DrawerBindableInput
|
<DrawerBindableInput
|
||||||
fillWidth={true}
|
fillWidth
|
||||||
title={value.title}
|
title={value.title}
|
||||||
panel={AutomationBindingPanel}
|
panel={AutomationBindingPanel}
|
||||||
type={value.customType}
|
type="email"
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange(e, key)}
|
||||||
{bindings}
|
{bindings}
|
||||||
|
allowJS={false}
|
||||||
updateOnChange={false}
|
updateOnChange={false}
|
||||||
placeholder={value.customType === "queryLimit" ? queryLimit : ""}
|
|
||||||
drawerLeft="260px"
|
drawerLeft="260px"
|
||||||
/>
|
/>
|
||||||
</div>
|
{/if}
|
||||||
|
{:else if value.customType === "query"}
|
||||||
|
<QuerySelector
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
value={inputData[key]}
|
||||||
|
/>
|
||||||
|
{:else if value.customType === "cron"}
|
||||||
|
<CronBuilder
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
value={inputData[key]}
|
||||||
|
/>
|
||||||
|
{:else if value.customType === "queryParams"}
|
||||||
|
<QueryParamSelector
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
value={inputData[key]}
|
||||||
|
{bindings}
|
||||||
|
/>
|
||||||
|
{:else if value.customType === "table"}
|
||||||
|
<TableSelector
|
||||||
|
{isTrigger}
|
||||||
|
value={inputData[key]}
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
/>
|
||||||
|
{:else if value.customType === "row"}
|
||||||
|
<RowSelector
|
||||||
|
{block}
|
||||||
|
value={inputData[key]}
|
||||||
|
meta={inputData["meta"] || {}}
|
||||||
|
on:change={e => {
|
||||||
|
if (e.detail?.key) {
|
||||||
|
onChange(e, e.detail.key)
|
||||||
|
} else {
|
||||||
|
onChange(e, key)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{bindings}
|
||||||
|
{isTestModal}
|
||||||
|
{isUpdateRow}
|
||||||
|
/>
|
||||||
|
{:else if value.customType === "webhookUrl"}
|
||||||
|
<WebhookDisplay
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
value={inputData[key]}
|
||||||
|
/>
|
||||||
|
{:else if value.customType === "fields"}
|
||||||
|
<FieldSelector
|
||||||
|
{block}
|
||||||
|
value={inputData[key]}
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
{bindings}
|
||||||
|
{isTestModal}
|
||||||
|
/>
|
||||||
|
{:else if value.customType === "triggerSchema"}
|
||||||
|
<SchemaSetup
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
value={inputData[key]}
|
||||||
|
/>
|
||||||
|
{:else if value.customType === "code"}
|
||||||
|
<CodeEditorModal>
|
||||||
|
<CodeEditor
|
||||||
|
value={inputData[key]}
|
||||||
|
on:change={e => {
|
||||||
|
// need to pass without the value inside
|
||||||
|
onChange({ detail: e.detail }, key)
|
||||||
|
inputData[key] = e.detail
|
||||||
|
}}
|
||||||
|
completions={[
|
||||||
|
jsAutocomplete([
|
||||||
|
...bindingsToCompletions(bindings, EditorModes.JS),
|
||||||
|
]),
|
||||||
|
]}
|
||||||
|
mode={EditorModes.JS}
|
||||||
|
height={500}
|
||||||
|
/>
|
||||||
|
<div class="messaging">
|
||||||
|
<Icon name="FlashOn" />
|
||||||
|
<div class="messaging-wrap">
|
||||||
|
<div>Add available bindings by typing <strong>$</strong></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CodeEditorModal>
|
||||||
|
{:else if value.customType === "loopOption"}
|
||||||
|
<Select
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
autoWidth
|
||||||
|
value={inputData[key]}
|
||||||
|
options={["Array", "String"]}
|
||||||
|
defaultValue={"Array"}
|
||||||
|
/>
|
||||||
|
{:else if value.type === "string" || value.type === "number" || value.type === "integer"}
|
||||||
|
{#if isTestModal}
|
||||||
|
<ModalBindableInput
|
||||||
|
title={value.title}
|
||||||
|
value={inputData[key]}
|
||||||
|
panel={AutomationBindingPanel}
|
||||||
|
type={value.customType}
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
{bindings}
|
||||||
|
updateOnChange={false}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div class="test">
|
||||||
|
<DrawerBindableInput
|
||||||
|
fillWidth={true}
|
||||||
|
title={value.title}
|
||||||
|
panel={AutomationBindingPanel}
|
||||||
|
type={value.customType}
|
||||||
|
value={inputData[key]}
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
{bindings}
|
||||||
|
updateOnChange={false}
|
||||||
|
placeholder={value.customType === "queryLimit"
|
||||||
|
? queryLimit
|
||||||
|
: ""}
|
||||||
|
drawerLeft="260px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<Modal bind:this={webhookModal} width="30%">
|
<Modal bind:this={webhookModal} width="30%">
|
||||||
|
|
|
@ -3,8 +3,6 @@
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import { auth } from "stores/portal"
|
import { auth } from "stores/portal"
|
||||||
|
|
||||||
export let preAuthStep
|
|
||||||
export let datasource
|
|
||||||
export let disabled
|
export let disabled
|
||||||
export let samePage
|
export let samePage
|
||||||
|
|
||||||
|
@ -15,18 +13,8 @@
|
||||||
class:disabled
|
class:disabled
|
||||||
{disabled}
|
{disabled}
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
let ds = datasource
|
|
||||||
let appId = $store.appId
|
let appId = $store.appId
|
||||||
if (!ds) {
|
const url = `/api/global/auth/${tenantId}/datasource/google?appId=${appId}`
|
||||||
const resp = await preAuthStep()
|
|
||||||
if (resp.datasource && resp.appId) {
|
|
||||||
ds = resp.datasource
|
|
||||||
appId = resp.appId
|
|
||||||
} else {
|
|
||||||
ds = resp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const url = `/api/global/auth/${tenantId}/datasource/google?datasourceId=${ds._id}&appId=${appId}`
|
|
||||||
if (samePage) {
|
if (samePage) {
|
||||||
window.location = url
|
window.location = url
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,43 +1,110 @@
|
||||||
<script>
|
<script>
|
||||||
import { ModalContent, Body, Layout, Link } from "@budibase/bbui"
|
import {
|
||||||
import { IntegrationNames } from "constants/backend"
|
ModalContent,
|
||||||
import cloneDeep from "lodash/cloneDeepWith"
|
Body,
|
||||||
|
Layout,
|
||||||
|
Link,
|
||||||
|
notifications,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { IntegrationNames, IntegrationTypes } from "constants/backend"
|
||||||
import GoogleButton from "../_components/GoogleButton.svelte"
|
import GoogleButton from "../_components/GoogleButton.svelte"
|
||||||
import { saveDatasource as save } from "builderStore/datasource"
|
|
||||||
import { organisation } from "stores/portal"
|
import { organisation } from "stores/portal"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
import { validateDatasourceConfig } from "builderStore/datasource"
|
||||||
|
import cloneDeep from "lodash/cloneDeepWith"
|
||||||
|
import IntegrationConfigForm from "../TableIntegrationMenu/IntegrationConfigForm.svelte"
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
|
||||||
|
import { saveDatasource } from "builderStore/datasource"
|
||||||
|
import { DatasourceFeature } from "@budibase/types"
|
||||||
|
|
||||||
export let integration
|
export let integration
|
||||||
|
export let continueSetupId = false
|
||||||
|
|
||||||
// kill the reference so the input isn't saved
|
|
||||||
let datasource = cloneDeep(integration)
|
let datasource = cloneDeep(integration)
|
||||||
|
datasource.config.continueSetupId = continueSetupId
|
||||||
|
|
||||||
$: isGoogleConfigured = !!$organisation.googleDatasourceConfigured
|
$: isGoogleConfigured = !!$organisation.googleDatasourceConfigured
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await organisation.init()
|
await organisation.init()
|
||||||
})
|
})
|
||||||
|
const integrationName = IntegrationNames[IntegrationTypes.GOOGLE_SHEETS]
|
||||||
|
|
||||||
|
export const GoogleDatasouceConfigStep = {
|
||||||
|
AUTH: "Auth",
|
||||||
|
SET_URL: "Set_url",
|
||||||
|
}
|
||||||
|
|
||||||
|
let step = continueSetupId
|
||||||
|
? GoogleDatasouceConfigStep.SET_URL
|
||||||
|
: GoogleDatasouceConfigStep.AUTH
|
||||||
|
|
||||||
|
let isValid = false
|
||||||
|
|
||||||
|
const modalConfig = {
|
||||||
|
[GoogleDatasouceConfigStep.AUTH]: {},
|
||||||
|
[GoogleDatasouceConfigStep.SET_URL]: {
|
||||||
|
confirmButtonText: "Connect",
|
||||||
|
onConfirm: async () => {
|
||||||
|
if (integration.features[DatasourceFeature.CONNECTION_CHECKING]) {
|
||||||
|
const resp = await validateDatasourceConfig(datasource)
|
||||||
|
if (!resp.connected) {
|
||||||
|
notifications.error(`Unable to connect - ${resp.error}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await saveDatasource(datasource)
|
||||||
|
$goto(`./datasource/${resp._id}`)
|
||||||
|
notifications.success(`Datasource created successfully.`)
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error(err?.message ?? "Error saving datasource")
|
||||||
|
// prevent the modal from closing
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title={`Connect to ${IntegrationNames[datasource.type]}`}
|
title={`Connect to ${integrationName}`}
|
||||||
cancelText="Back"
|
cancelText="Cancel"
|
||||||
size="L"
|
size="L"
|
||||||
|
confirmText={modalConfig[step].confirmButtonText}
|
||||||
|
showConfirmButton={!!modalConfig[step].onConfirm}
|
||||||
|
onConfirm={modalConfig[step].onConfirm}
|
||||||
|
disabled={!isValid}
|
||||||
>
|
>
|
||||||
<!-- check true and false directly, don't render until flag is set -->
|
{#if step === GoogleDatasouceConfigStep.AUTH}
|
||||||
{#if isGoogleConfigured === true}
|
<!-- check true and false directly, don't render until flag is set -->
|
||||||
<Layout noPadding>
|
{#if isGoogleConfigured === true}
|
||||||
|
<Layout noPadding>
|
||||||
|
<Body size="S"
|
||||||
|
>Authenticate with your google account to use the {integrationName} integration.</Body
|
||||||
|
>
|
||||||
|
</Layout>
|
||||||
|
<GoogleButton samePage />
|
||||||
|
{:else if isGoogleConfigured === false}
|
||||||
<Body size="S"
|
<Body size="S"
|
||||||
>Authenticate with your google account to use the {IntegrationNames[
|
>Google authentication is not enabled, please complete Google SSO
|
||||||
datasource.type
|
configuration.</Body
|
||||||
]} integration.</Body
|
|
||||||
>
|
>
|
||||||
|
<Link href="/builder/portal/settings/auth">Configure Google SSO</Link>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{#if step === GoogleDatasouceConfigStep.SET_URL}
|
||||||
|
<Layout noPadding no>
|
||||||
|
<Body size="S">Add the URL of the sheet you want to connect.</Body>
|
||||||
|
|
||||||
|
<IntegrationConfigForm
|
||||||
|
schema={datasource.schema}
|
||||||
|
bind:datasource
|
||||||
|
creating={true}
|
||||||
|
on:valid={e => (isValid = e.detail)}
|
||||||
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
<GoogleButton preAuthStep={() => save(datasource, true)} />
|
|
||||||
{:else if isGoogleConfigured === false}
|
|
||||||
<Body size="S"
|
|
||||||
>Google authentication is not enabled, please complete Google SSO
|
|
||||||
configuration.</Body
|
|
||||||
>
|
|
||||||
<Link href="/builder/portal/settings/auth">Configure Google SSO</Link>
|
|
||||||
{/if}
|
{/if}
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte"
|
import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte"
|
||||||
import ICONS from "components/backend/DatasourceNavigator/icons/index.js"
|
import ICONS from "components/backend/DatasourceNavigator/icons/index.js"
|
||||||
import FontAwesomeIcon from "components/common/FontAwesomeIcon.svelte"
|
import FontAwesomeIcon from "components/common/FontAwesomeIcon.svelte"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
let internalTableModal
|
let internalTableModal
|
||||||
let externalDatasourceModal
|
let externalDatasourceModal
|
||||||
|
@ -129,9 +130,19 @@
|
||||||
return integrationsArray
|
return integrationsArray
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let continueGoogleSetup
|
||||||
|
onMount(() => {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
|
continueGoogleSetup = urlParams.get("continue_google_setup")
|
||||||
|
})
|
||||||
|
|
||||||
const fetchIntegrations = async () => {
|
const fetchIntegrations = async () => {
|
||||||
const unsortedIntegrations = await API.getIntegrations()
|
const unsortedIntegrations = await API.getIntegrations()
|
||||||
integrations = sortIntegrations(unsortedIntegrations)
|
integrations = sortIntegrations(unsortedIntegrations)
|
||||||
|
|
||||||
|
if (continueGoogleSetup) {
|
||||||
|
handleIntegrationSelect(IntegrationTypes.GOOGLE_SHEETS)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: fetchIntegrations()
|
$: fetchIntegrations()
|
||||||
|
@ -141,9 +152,17 @@
|
||||||
<CreateTableModal {promptUpload} afterSave={handleInternalTableSave} />
|
<CreateTableModal {promptUpload} afterSave={handleInternalTableSave} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={externalDatasourceModal}>
|
<Modal
|
||||||
|
bind:this={externalDatasourceModal}
|
||||||
|
on:hide={() => {
|
||||||
|
continueGoogleSetup = null
|
||||||
|
}}
|
||||||
|
>
|
||||||
{#if integration?.auth?.type === "google"}
|
{#if integration?.auth?.type === "google"}
|
||||||
<GoogleDatasourceConfigModal {integration} />
|
<GoogleDatasourceConfigModal
|
||||||
|
continueSetupId={continueGoogleSetup}
|
||||||
|
{integration}
|
||||||
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<DatasourceConfigModal {integration} />
|
<DatasourceConfigModal {integration} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -0,0 +1,235 @@
|
||||||
|
<script>
|
||||||
|
import GoogleLogo from "./_logos/Google.svelte"
|
||||||
|
import { isEqual, cloneDeep } from "lodash/fp"
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Heading,
|
||||||
|
Divider,
|
||||||
|
Label,
|
||||||
|
notifications,
|
||||||
|
Layout,
|
||||||
|
Input,
|
||||||
|
Body,
|
||||||
|
Toggle,
|
||||||
|
Icon,
|
||||||
|
Helpers,
|
||||||
|
Link,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { API } from "api"
|
||||||
|
import { organisation, admin } from "stores/portal"
|
||||||
|
|
||||||
|
const ConfigTypes = {
|
||||||
|
Google: "google",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some older google configs contain a manually specified value - retain the functionality to edit the field
|
||||||
|
// When there is no value or we are in the cloud - prohibit editing the field, must use platform url to change
|
||||||
|
$: googleCallbackUrl = undefined
|
||||||
|
$: googleCallbackReadonly = $admin.cloud || !googleCallbackUrl
|
||||||
|
|
||||||
|
// Indicate to user that callback is based on platform url
|
||||||
|
// If there is an existing value, indicate that it may be removed to return to default behaviour
|
||||||
|
$: googleCallbackTooltip = $admin.cloud
|
||||||
|
? null
|
||||||
|
: googleCallbackReadonly
|
||||||
|
? "Visit the organisation page to update the platform URL"
|
||||||
|
: "Leave blank to use the default callback URL"
|
||||||
|
$: googleSheetsCallbackUrl = `${$organisation.platformUrl}/api/global/auth/datasource/google/callback`
|
||||||
|
|
||||||
|
$: GoogleConfigFields = {
|
||||||
|
Google: [
|
||||||
|
{ name: "clientID", label: "Client ID" },
|
||||||
|
{ name: "clientSecret", label: "Client secret" },
|
||||||
|
{
|
||||||
|
name: "callbackURL",
|
||||||
|
label: "Callback URL",
|
||||||
|
readonly: googleCallbackReadonly,
|
||||||
|
tooltip: googleCallbackTooltip,
|
||||||
|
placeholder: $organisation.googleCallbackUrl,
|
||||||
|
copyButton: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sheetsURL",
|
||||||
|
label: "Sheets URL",
|
||||||
|
readonly: googleCallbackReadonly,
|
||||||
|
tooltip: googleCallbackTooltip,
|
||||||
|
placeholder: googleSheetsCallbackUrl,
|
||||||
|
copyButton: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
let google
|
||||||
|
|
||||||
|
const providers = { google }
|
||||||
|
|
||||||
|
// control the state of the save button depending on whether form has changed
|
||||||
|
let originalGoogleDoc
|
||||||
|
let googleSaveButtonDisabled
|
||||||
|
$: {
|
||||||
|
isEqual(providers.google?.config, originalGoogleDoc?.config)
|
||||||
|
? (googleSaveButtonDisabled = true)
|
||||||
|
: (googleSaveButtonDisabled = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
$: googleComplete = !!(
|
||||||
|
providers.google?.config?.clientID && providers.google?.config?.clientSecret
|
||||||
|
)
|
||||||
|
|
||||||
|
async function saveConfig(config) {
|
||||||
|
// Delete unsupported fields
|
||||||
|
delete config.createdAt
|
||||||
|
delete config.updatedAt
|
||||||
|
return API.saveConfig(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveGoogle() {
|
||||||
|
if (!googleComplete) {
|
||||||
|
notifications.error(
|
||||||
|
`Please fill in all required ${ConfigTypes.Google} fields`
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const google = providers.google
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await saveConfig(google)
|
||||||
|
providers[res.type]._rev = res._rev
|
||||||
|
providers[res.type]._id = res._id
|
||||||
|
notifications.success(`Settings saved`)
|
||||||
|
} catch (e) {
|
||||||
|
notifications.error(e.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
googleSaveButtonDisabled = true
|
||||||
|
originalGoogleDoc = cloneDeep(providers.google)
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyToClipboard = async value => {
|
||||||
|
await Helpers.copyToClipboard(value)
|
||||||
|
notifications.success("Copied")
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
await organisation.init()
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error getting org config")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch Google config
|
||||||
|
let googleDoc
|
||||||
|
try {
|
||||||
|
googleDoc = await API.getConfig(ConfigTypes.Google)
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error fetching Google OAuth config")
|
||||||
|
}
|
||||||
|
if (!googleDoc?._id) {
|
||||||
|
providers.google = {
|
||||||
|
type: ConfigTypes.Google,
|
||||||
|
config: { activated: false },
|
||||||
|
}
|
||||||
|
originalGoogleDoc = cloneDeep(googleDoc)
|
||||||
|
} else {
|
||||||
|
// Default activated to true for older configs
|
||||||
|
if (googleDoc.config.activated === undefined) {
|
||||||
|
googleDoc.config.activated = true
|
||||||
|
}
|
||||||
|
originalGoogleDoc = cloneDeep(googleDoc)
|
||||||
|
providers.google = googleDoc
|
||||||
|
}
|
||||||
|
googleCallbackUrl = providers?.google?.config?.callbackURL
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if providers.google}
|
||||||
|
<Divider />
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<Heading size="S">
|
||||||
|
<div class="provider-title">
|
||||||
|
<GoogleLogo />
|
||||||
|
<span>Google</span>
|
||||||
|
</div>
|
||||||
|
</Heading>
|
||||||
|
<Body size="S">
|
||||||
|
To allow users to authenticate using their Google accounts, fill out the
|
||||||
|
fields below. Read the <Link
|
||||||
|
size="M"
|
||||||
|
href={"https://docs.budibase.com/docs/sso-with-google"}
|
||||||
|
>documentation</Link
|
||||||
|
> for more information.
|
||||||
|
</Body>
|
||||||
|
</Layout>
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
{#each GoogleConfigFields.Google as field}
|
||||||
|
<div class="form-row">
|
||||||
|
<Label size="L" tooltip={field.tooltip}>{field.label}</Label>
|
||||||
|
<div class="inputContainer">
|
||||||
|
<div class="input">
|
||||||
|
<Input
|
||||||
|
bind:value={providers.google.config[field.name]}
|
||||||
|
readonly={field.readonly}
|
||||||
|
placeholder={field.placeholder}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{#if field.copyButton}
|
||||||
|
<div
|
||||||
|
class="copy"
|
||||||
|
on:click={() => copyToClipboard(field.placeholder)}
|
||||||
|
>
|
||||||
|
<Icon size="S" name="Copy" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<div class="form-row">
|
||||||
|
<Label size="L">Activated</Label>
|
||||||
|
<Toggle text="" bind:value={providers.google.config.activated} />
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
disabled={googleSaveButtonDisabled}
|
||||||
|
cta
|
||||||
|
on:click={() => saveGoogle()}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 120px 1fr;
|
||||||
|
grid-gap: var(--spacing-l);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.provider-title span {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
.inputContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
.input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.copy {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,5 +1,4 @@
|
||||||
<script>
|
<script>
|
||||||
import GoogleLogo from "./_logos/Google.svelte"
|
|
||||||
import OidcLogo from "./_logos/OIDC.svelte"
|
import OidcLogo from "./_logos/OIDC.svelte"
|
||||||
import MicrosoftLogo from "assets/microsoft-logo.png"
|
import MicrosoftLogo from "assets/microsoft-logo.png"
|
||||||
import Auth0Logo from "assets/auth0-logo.png"
|
import Auth0Logo from "assets/auth0-logo.png"
|
||||||
|
@ -28,9 +27,9 @@
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { organisation, admin, licensing } from "stores/portal"
|
import { organisation, admin, licensing } from "stores/portal"
|
||||||
import Scim from "./scim.svelte"
|
import Scim from "./scim.svelte"
|
||||||
|
import Google from "./google.svelte"
|
||||||
|
|
||||||
const ConfigTypes = {
|
const ConfigTypes = {
|
||||||
Google: "google",
|
|
||||||
OIDC: "oidc",
|
OIDC: "oidc",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,43 +37,6 @@
|
||||||
|
|
||||||
$: enforcedSSO = $organisation.isSSOEnforced
|
$: enforcedSSO = $organisation.isSSOEnforced
|
||||||
|
|
||||||
// Some older google configs contain a manually specified value - retain the functionality to edit the field
|
|
||||||
// When there is no value or we are in the cloud - prohibit editing the field, must use platform url to change
|
|
||||||
$: googleCallbackUrl = undefined
|
|
||||||
$: googleCallbackReadonly = $admin.cloud || !googleCallbackUrl
|
|
||||||
|
|
||||||
// Indicate to user that callback is based on platform url
|
|
||||||
// If there is an existing value, indicate that it may be removed to return to default behaviour
|
|
||||||
$: googleCallbackTooltip = $admin.cloud
|
|
||||||
? null
|
|
||||||
: googleCallbackReadonly
|
|
||||||
? "Visit the organisation page to update the platform URL"
|
|
||||||
: "Leave blank to use the default callback URL"
|
|
||||||
$: googleSheetsCallbackUrl = `${$organisation.platformUrl}/api/global/auth/datasource/google/callback`
|
|
||||||
|
|
||||||
$: GoogleConfigFields = {
|
|
||||||
Google: [
|
|
||||||
{ name: "clientID", label: "Client ID" },
|
|
||||||
{ name: "clientSecret", label: "Client secret" },
|
|
||||||
{
|
|
||||||
name: "callbackURL",
|
|
||||||
label: "Callback URL",
|
|
||||||
readonly: googleCallbackReadonly,
|
|
||||||
tooltip: googleCallbackTooltip,
|
|
||||||
placeholder: $organisation.googleCallbackUrl,
|
|
||||||
copyButton: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "sheetsURL",
|
|
||||||
label: "Sheets URL",
|
|
||||||
readonly: googleCallbackReadonly,
|
|
||||||
tooltip: googleCallbackTooltip,
|
|
||||||
placeholder: googleSheetsCallbackUrl,
|
|
||||||
copyButton: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
$: OIDCConfigFields = {
|
$: OIDCConfigFields = {
|
||||||
Oidc: [
|
Oidc: [
|
||||||
{ name: "configUrl", label: "Config URL" },
|
{ name: "configUrl", label: "Config URL" },
|
||||||
|
@ -133,15 +95,9 @@
|
||||||
const providers = { google, oidc }
|
const providers = { google, oidc }
|
||||||
|
|
||||||
// control the state of the save button depending on whether form has changed
|
// control the state of the save button depending on whether form has changed
|
||||||
let originalGoogleDoc
|
|
||||||
let originalOidcDoc
|
let originalOidcDoc
|
||||||
let googleSaveButtonDisabled
|
|
||||||
let oidcSaveButtonDisabled
|
let oidcSaveButtonDisabled
|
||||||
$: {
|
$: {
|
||||||
isEqual(providers.google?.config, originalGoogleDoc?.config)
|
|
||||||
? (googleSaveButtonDisabled = true)
|
|
||||||
: (googleSaveButtonDisabled = false)
|
|
||||||
|
|
||||||
// delete the callback url which is never saved to the oidc
|
// delete the callback url which is never saved to the oidc
|
||||||
// config doc, to ensure an accurate comparison
|
// config doc, to ensure an accurate comparison
|
||||||
delete providers.oidc?.config.configs[0].callbackURL
|
delete providers.oidc?.config.configs[0].callbackURL
|
||||||
|
@ -151,10 +107,6 @@
|
||||||
: (oidcSaveButtonDisabled = false)
|
: (oidcSaveButtonDisabled = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
$: googleComplete = !!(
|
|
||||||
providers.google?.config?.clientID && providers.google?.config?.clientSecret
|
|
||||||
)
|
|
||||||
|
|
||||||
$: oidcComplete = !!(
|
$: oidcComplete = !!(
|
||||||
providers.oidc?.config?.configs[0].configUrl &&
|
providers.oidc?.config?.configs[0].configUrl &&
|
||||||
providers.oidc?.config?.configs[0].clientID &&
|
providers.oidc?.config?.configs[0].clientID &&
|
||||||
|
@ -230,30 +182,6 @@
|
||||||
originalOidcDoc = cloneDeep(providers.oidc)
|
originalOidcDoc = cloneDeep(providers.oidc)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveGoogle() {
|
|
||||||
if (!googleComplete) {
|
|
||||||
notifications.error(
|
|
||||||
`Please fill in all required ${ConfigTypes.Google} fields`
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const google = providers.google
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await saveConfig(google)
|
|
||||||
providers[res.type]._rev = res._rev
|
|
||||||
providers[res.type]._id = res._id
|
|
||||||
notifications.success(`Settings saved`)
|
|
||||||
} catch (e) {
|
|
||||||
notifications.error(e.message)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
googleSaveButtonDisabled = true
|
|
||||||
originalGoogleDoc = cloneDeep(providers.google)
|
|
||||||
}
|
|
||||||
|
|
||||||
let defaultScopes = ["profile", "email", "offline_access"]
|
let defaultScopes = ["profile", "email", "offline_access"]
|
||||||
|
|
||||||
const refreshScopes = idx => {
|
const refreshScopes = idx => {
|
||||||
|
@ -281,29 +209,6 @@
|
||||||
notifications.error("Error getting org config")
|
notifications.error("Error getting org config")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch Google config
|
|
||||||
let googleDoc
|
|
||||||
try {
|
|
||||||
googleDoc = await API.getConfig(ConfigTypes.Google)
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error fetching Google OAuth config")
|
|
||||||
}
|
|
||||||
if (!googleDoc?._id) {
|
|
||||||
providers.google = {
|
|
||||||
type: ConfigTypes.Google,
|
|
||||||
config: { activated: false },
|
|
||||||
}
|
|
||||||
originalGoogleDoc = cloneDeep(googleDoc)
|
|
||||||
} else {
|
|
||||||
// Default activated to true for older configs
|
|
||||||
if (googleDoc.config.activated === undefined) {
|
|
||||||
googleDoc.config.activated = true
|
|
||||||
}
|
|
||||||
originalGoogleDoc = cloneDeep(googleDoc)
|
|
||||||
providers.google = googleDoc
|
|
||||||
}
|
|
||||||
googleCallbackUrl = providers?.google?.config?.callbackURL
|
|
||||||
|
|
||||||
// Get the list of user uploaded logos and push it to the dropdown options.
|
// Get the list of user uploaded logos and push it to the dropdown options.
|
||||||
// This needs to be done before the config call so they're available when
|
// This needs to be done before the config call so they're available when
|
||||||
// the dropdown renders.
|
// the dropdown renders.
|
||||||
|
@ -395,62 +300,7 @@
|
||||||
> before enabling this feature.
|
> before enabling this feature.
|
||||||
</Body>
|
</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
{#if providers.google}
|
<Google />
|
||||||
<Divider />
|
|
||||||
<Layout gap="XS" noPadding>
|
|
||||||
<Heading size="S">
|
|
||||||
<div class="provider-title">
|
|
||||||
<GoogleLogo />
|
|
||||||
<span>Google</span>
|
|
||||||
</div>
|
|
||||||
</Heading>
|
|
||||||
<Body size="S">
|
|
||||||
To allow users to authenticate using their Google accounts, fill out the
|
|
||||||
fields below. Read the <Link
|
|
||||||
size="M"
|
|
||||||
href={"https://docs.budibase.com/docs/sso-with-google"}
|
|
||||||
>documentation</Link
|
|
||||||
> for more information.
|
|
||||||
</Body>
|
|
||||||
</Layout>
|
|
||||||
<Layout gap="XS" noPadding>
|
|
||||||
{#each GoogleConfigFields.Google as field}
|
|
||||||
<div class="form-row">
|
|
||||||
<Label size="L" tooltip={field.tooltip}>{field.label}</Label>
|
|
||||||
<div class="inputContainer">
|
|
||||||
<div class="input">
|
|
||||||
<Input
|
|
||||||
bind:value={providers.google.config[field.name]}
|
|
||||||
readonly={field.readonly}
|
|
||||||
placeholder={field.placeholder}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{#if field.copyButton}
|
|
||||||
<div
|
|
||||||
class="copy"
|
|
||||||
on:click={() => copyToClipboard(field.placeholder)}
|
|
||||||
>
|
|
||||||
<Icon size="S" name="Copy" />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
<div class="form-row">
|
|
||||||
<Label size="L">Activated</Label>
|
|
||||||
<Toggle text="" bind:value={providers.google.config.activated} />
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
disabled={googleSaveButtonDisabled}
|
|
||||||
cta
|
|
||||||
on:click={() => saveGoogle()}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if providers.oidc}
|
{#if providers.oidc}
|
||||||
<Divider />
|
<Divider />
|
||||||
<Layout gap="XS" noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit cd06642b860111aa1bd3443ee10076ca3abf03c3
|
Subproject commit 01fbc8670021c5a275c2a1a36ee18b984eeafad5
|
|
@ -14,7 +14,7 @@ ENV SERVICE=app-service
|
||||||
ENV POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU
|
ENV POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU
|
||||||
ENV TENANT_FEATURE_FLAGS=*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR
|
ENV TENANT_FEATURE_FLAGS=*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR
|
||||||
ENV ACCOUNT_PORTAL_URL=https://account.budibase.app
|
ENV ACCOUNT_PORTAL_URL=https://account.budibase.app
|
||||||
|
ENV TOP_LEVEL_PATH=/
|
||||||
|
|
||||||
# handle node-gyp
|
# handle node-gyp
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
|
@ -27,7 +27,8 @@ COPY scripts/integrations/oracle/ scripts/integrations/oracle/
|
||||||
RUN /bin/bash -e scripts/integrations/oracle/instantclient/linux/x86-64/install.sh
|
RUN /bin/bash -e scripts/integrations/oracle/instantclient/linux/x86-64/install.sh
|
||||||
|
|
||||||
COPY package.json .
|
COPY package.json .
|
||||||
RUN yarn install --frozen-lockfile --production=true
|
COPY dist/yarn.lock .
|
||||||
|
RUN yarn install --production=true
|
||||||
# Remove unneeded data from file system to reduce image size
|
# Remove unneeded data from file system to reduce image size
|
||||||
RUN yarn cache clean && apt-get remove -y --purge --auto-remove g++ make python \
|
RUN yarn cache clean && apt-get remove -y --purge --auto-remove g++ make python \
|
||||||
&& rm -rf /tmp/* /root/.node-gyp /usr/local/lib/node_modules/npm/node_modules/node-gyp
|
&& rm -rf /tmp/* /root/.node-gyp /usr/local/lib/node_modules/npm/node_modules/node-gyp
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
"test": "bash scripts/test.sh",
|
"test": "bash scripts/test.sh",
|
||||||
"test:memory": "jest --maxWorkers=2 --logHeapUsage --forceExit",
|
"test:memory": "jest --maxWorkers=2 --logHeapUsage --forceExit",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"predocker": "copyfiles -f ../client/dist/budibase-client.js ../client/manifest.json client && yarn build --configuration=production",
|
"predocker": "copyfiles -f ../client/dist/budibase-client.js ../client/manifest.json client && yarn build && cp ../../yarn.lock ./dist/",
|
||||||
"build:docker": "yarn predocker && docker build . -t app-service --label version=$BUDIBASE_RELEASE_VERSION",
|
"build:docker": "yarn predocker && docker build . -t app-service --label version=$BUDIBASE_RELEASE_VERSION",
|
||||||
"build:docs": "node ./scripts/docs/generate.js open",
|
"build:docs": "node ./scripts/docs/generate.js open",
|
||||||
"run:docker": "node dist/index.js",
|
"run:docker": "node dist/index.js",
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { BuildSchemaErrors, InvalidColumns } from "../../constants"
|
||||||
import { getIntegration } from "../../integrations"
|
import { getIntegration } from "../../integrations"
|
||||||
import { getDatasourceAndQuery } from "./row/utils"
|
import { getDatasourceAndQuery } from "./row/utils"
|
||||||
import { invalidateDynamicVariables } from "../../threads/utils"
|
import { invalidateDynamicVariables } from "../../threads/utils"
|
||||||
import { db as dbCore, context, events } from "@budibase/backend-core"
|
import { db as dbCore, context, events, cache } from "@budibase/backend-core"
|
||||||
import {
|
import {
|
||||||
UserCtx,
|
UserCtx,
|
||||||
Datasource,
|
Datasource,
|
||||||
|
@ -25,9 +25,11 @@ import {
|
||||||
FetchDatasourceInfoResponse,
|
FetchDatasourceInfoResponse,
|
||||||
IntegrationBase,
|
IntegrationBase,
|
||||||
DatasourcePlus,
|
DatasourcePlus,
|
||||||
|
SourceName,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import sdk from "../../sdk"
|
import sdk from "../../sdk"
|
||||||
import { builderSocket } from "../../websockets"
|
import { builderSocket } from "../../websockets"
|
||||||
|
import { setupCreationAuth as googleSetupCreationAuth } from "../../integrations/googlesheets"
|
||||||
|
|
||||||
function getErrorTables(errors: any, errorType: string) {
|
function getErrorTables(errors: any, errorType: string) {
|
||||||
return Object.entries(errors)
|
return Object.entries(errors)
|
||||||
|
@ -306,6 +308,12 @@ export async function update(ctx: UserCtx<any, UpdateDatasourceResponse>) {
|
||||||
builderSocket?.emitDatasourceUpdate(ctx, datasource)
|
builderSocket?.emitDatasourceUpdate(ctx, datasource)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const preSaveAction: Partial<Record<SourceName, any>> = {
|
||||||
|
[SourceName.GOOGLE_SHEETS]: async (datasource: Datasource) => {
|
||||||
|
await googleSetupCreationAuth(datasource.config as any)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
export async function save(
|
export async function save(
|
||||||
ctx: UserCtx<CreateDatasourceRequest, CreateDatasourceResponse>
|
ctx: UserCtx<CreateDatasourceRequest, CreateDatasourceResponse>
|
||||||
) {
|
) {
|
||||||
|
@ -327,6 +335,10 @@ export async function save(
|
||||||
setDefaultDisplayColumns(datasource)
|
setDefaultDisplayColumns(datasource)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (preSaveAction[datasource.source]) {
|
||||||
|
await preSaveAction[datasource.source](datasource)
|
||||||
|
}
|
||||||
|
|
||||||
const dbResp = await db.put(datasource)
|
const dbResp = await db.put(datasource)
|
||||||
await events.datasource.created(datasource)
|
await events.datasource.created(datasource)
|
||||||
datasource._rev = dbResp.rev
|
datasource._rev = dbResp.rev
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
require("svelte/register")
|
require("svelte/register")
|
||||||
|
|
||||||
import { resolve, join } from "../../../utilities/centralPath"
|
import { join } from "../../../utilities/centralPath"
|
||||||
const uuid = require("uuid")
|
const uuid = require("uuid")
|
||||||
import { ObjectStoreBuckets } from "../../../constants"
|
import { ObjectStoreBuckets } from "../../../constants"
|
||||||
import { processString } from "@budibase/string-templates"
|
import { processString } from "@budibase/string-templates"
|
||||||
|
@ -49,7 +49,7 @@ export const toggleBetaUiFeature = async function (ctx: any) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let builderPath = resolve(TOP_LEVEL_PATH, "new_design_ui")
|
let builderPath = join(TOP_LEVEL_PATH, "new_design_ui")
|
||||||
|
|
||||||
// // download it from S3
|
// // download it from S3
|
||||||
if (!fs.existsSync(builderPath)) {
|
if (!fs.existsSync(builderPath)) {
|
||||||
|
@ -67,7 +67,7 @@ export const toggleBetaUiFeature = async function (ctx: any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const serveBuilder = async function (ctx: any) {
|
export const serveBuilder = async function (ctx: any) {
|
||||||
const builderPath = resolve(TOP_LEVEL_PATH, "builder")
|
const builderPath = join(TOP_LEVEL_PATH, "builder")
|
||||||
await send(ctx, ctx.file, { root: builderPath })
|
await send(ctx, ctx.file, { root: builderPath })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -208,6 +208,7 @@ export async function save(ctx: UserCtx) {
|
||||||
let tableToSave: TableRequest = {
|
let tableToSave: TableRequest = {
|
||||||
type: "table",
|
type: "table",
|
||||||
_id: buildExternalTableId(datasourceId, inputs.name),
|
_id: buildExternalTableId(datasourceId, inputs.name),
|
||||||
|
sourceId: datasourceId,
|
||||||
...inputs,
|
...inputs,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
SearchFilters,
|
SearchFilters,
|
||||||
Table,
|
Table,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
import { db as dbCore } from "@budibase/backend-core"
|
||||||
|
|
||||||
enum SortOrder {
|
enum SortOrder {
|
||||||
ASCENDING = "ascending",
|
ASCENDING = "ascending",
|
||||||
|
@ -121,7 +122,11 @@ function typeCoercion(filters: SearchFilters, table: Table) {
|
||||||
const searchParam = filters[key]
|
const searchParam = filters[key]
|
||||||
if (typeof searchParam === "object") {
|
if (typeof searchParam === "object") {
|
||||||
for (let [property, value] of Object.entries(searchParam)) {
|
for (let [property, value] of Object.entries(searchParam)) {
|
||||||
const column = table.schema[property]
|
// We need to strip numerical prefixes here, so that we can look up
|
||||||
|
// the correct field name in the schema
|
||||||
|
const columnName = dbCore.removeKeyNumbering(property)
|
||||||
|
const column = table.schema[columnName]
|
||||||
|
|
||||||
// convert string inputs
|
// convert string inputs
|
||||||
if (!column || typeof value !== "string") {
|
if (!column || typeof value !== "string") {
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -48,6 +48,35 @@ export const definition: AutomationStepSchema = {
|
||||||
type: AutomationIOType.STRING,
|
type: AutomationIOType.STRING,
|
||||||
title: "HTML Contents",
|
title: "HTML Contents",
|
||||||
},
|
},
|
||||||
|
addInvite: {
|
||||||
|
type: AutomationIOType.BOOLEAN,
|
||||||
|
title: "Add calendar invite",
|
||||||
|
},
|
||||||
|
startTime: {
|
||||||
|
type: AutomationIOType.DATE,
|
||||||
|
title: "Start Time",
|
||||||
|
dependsOn: "addInvite",
|
||||||
|
},
|
||||||
|
endTime: {
|
||||||
|
type: AutomationIOType.DATE,
|
||||||
|
title: "End Time",
|
||||||
|
dependsOn: "addInvite",
|
||||||
|
},
|
||||||
|
summary: {
|
||||||
|
type: AutomationIOType.STRING,
|
||||||
|
title: "Meeting Summary",
|
||||||
|
dependsOn: "addInvite",
|
||||||
|
},
|
||||||
|
location: {
|
||||||
|
type: AutomationIOType.STRING,
|
||||||
|
title: "Location",
|
||||||
|
dependsOn: "addInvite",
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
type: AutomationIOType.STRING,
|
||||||
|
title: "URL",
|
||||||
|
dependsOn: "addInvite",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
required: ["to", "from", "subject", "contents"],
|
required: ["to", "from", "subject", "contents"],
|
||||||
},
|
},
|
||||||
|
@ -68,21 +97,43 @@ export const definition: AutomationStepSchema = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function run({ inputs }: AutomationStepInput) {
|
export async function run({ inputs }: AutomationStepInput) {
|
||||||
let { to, from, subject, contents, cc, bcc } = inputs
|
let {
|
||||||
|
to,
|
||||||
|
from,
|
||||||
|
subject,
|
||||||
|
contents,
|
||||||
|
cc,
|
||||||
|
bcc,
|
||||||
|
addInvite,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
summary,
|
||||||
|
location,
|
||||||
|
url,
|
||||||
|
} = inputs
|
||||||
if (!contents) {
|
if (!contents) {
|
||||||
contents = "<h1>No content</h1>"
|
contents = "<h1>No content</h1>"
|
||||||
}
|
}
|
||||||
to = to || undefined
|
to = to || undefined
|
||||||
try {
|
try {
|
||||||
let response = await sendSmtpEmail(
|
let response = await sendSmtpEmail({
|
||||||
to,
|
to,
|
||||||
from,
|
from,
|
||||||
subject,
|
subject,
|
||||||
contents,
|
contents,
|
||||||
cc,
|
cc,
|
||||||
bcc,
|
bcc,
|
||||||
true
|
automation: true,
|
||||||
)
|
invite: addInvite
|
||||||
|
? {
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
summary,
|
||||||
|
location,
|
||||||
|
url,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
})
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
response,
|
response,
|
||||||
|
|
|
@ -1,71 +0,0 @@
|
||||||
|
|
||||||
function generateResponse(to, from) {
|
|
||||||
return {
|
|
||||||
"success": true,
|
|
||||||
"response": {
|
|
||||||
"accepted": [
|
|
||||||
to
|
|
||||||
],
|
|
||||||
"envelope": {
|
|
||||||
"from": from,
|
|
||||||
"to": [
|
|
||||||
to
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"message": `Email sent to ${to}.`
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockFetch = jest.fn(() => ({
|
|
||||||
headers: {
|
|
||||||
raw: () => {
|
|
||||||
return { "content-type": ["application/json"] }
|
|
||||||
},
|
|
||||||
get: () => ["application/json"],
|
|
||||||
},
|
|
||||||
json: jest.fn(() => response),
|
|
||||||
status: 200,
|
|
||||||
text: jest.fn(),
|
|
||||||
}))
|
|
||||||
jest.mock("node-fetch", () => mockFetch)
|
|
||||||
const setup = require("./utilities")
|
|
||||||
|
|
||||||
|
|
||||||
describe("test the outgoing webhook action", () => {
|
|
||||||
let inputs
|
|
||||||
let config = setup.getConfig()
|
|
||||||
beforeAll(async () => {
|
|
||||||
await config.init()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterAll(setup.afterAll)
|
|
||||||
|
|
||||||
it("should be able to run the action", async () => {
|
|
||||||
inputs = {
|
|
||||||
to: "user1@test.com",
|
|
||||||
from: "admin@test.com",
|
|
||||||
subject: "hello",
|
|
||||||
contents: "testing",
|
|
||||||
}
|
|
||||||
let resp = generateResponse(inputs.to, inputs.from)
|
|
||||||
mockFetch.mockImplementationOnce(() => ({
|
|
||||||
headers: {
|
|
||||||
raw: () => {
|
|
||||||
return { "content-type": ["application/json"] }
|
|
||||||
},
|
|
||||||
get: () => ["application/json"],
|
|
||||||
},
|
|
||||||
json: jest.fn(() => resp),
|
|
||||||
status: 200,
|
|
||||||
text: jest.fn(),
|
|
||||||
}))
|
|
||||||
const res = await setup.runStep(setup.actions.SEND_EMAIL_SMTP.stepId, inputs)
|
|
||||||
expect(res.response).toEqual(resp)
|
|
||||||
expect(res.success).toEqual(true)
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
})
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
import * as workerRequests from "../../utilities/workerRequests"
|
||||||
|
|
||||||
|
jest.mock("../../utilities/workerRequests", () => ({
|
||||||
|
sendSmtpEmail: jest.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
function generateResponse(to: string, from: string) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
response: {
|
||||||
|
accepted: [to],
|
||||||
|
envelope: {
|
||||||
|
from: from,
|
||||||
|
to: [to],
|
||||||
|
},
|
||||||
|
message: `Email sent to ${to}.`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setup = require("./utilities")
|
||||||
|
|
||||||
|
describe("test the outgoing webhook action", () => {
|
||||||
|
let inputs
|
||||||
|
let config = setup.getConfig()
|
||||||
|
beforeAll(async () => {
|
||||||
|
await config.init()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(setup.afterAll)
|
||||||
|
|
||||||
|
it("should be able to run the action", async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(workerRequests, "sendSmtpEmail")
|
||||||
|
.mockImplementationOnce(async () =>
|
||||||
|
generateResponse("user1@test.com", "admin@test.com")
|
||||||
|
)
|
||||||
|
const invite = {
|
||||||
|
startTime: new Date(),
|
||||||
|
endTime: new Date(),
|
||||||
|
summary: "summary",
|
||||||
|
location: "location",
|
||||||
|
url: "url",
|
||||||
|
}
|
||||||
|
inputs = {
|
||||||
|
to: "user1@test.com",
|
||||||
|
from: "admin@test.com",
|
||||||
|
subject: "hello",
|
||||||
|
contents: "testing",
|
||||||
|
cc: "cc",
|
||||||
|
bcc: "bcc",
|
||||||
|
addInvite: true,
|
||||||
|
...invite,
|
||||||
|
}
|
||||||
|
let resp = generateResponse(inputs.to, inputs.from)
|
||||||
|
const res = await setup.runStep(
|
||||||
|
setup.actions.SEND_EMAIL_SMTP.stepId,
|
||||||
|
inputs
|
||||||
|
)
|
||||||
|
expect(res.response).toEqual(resp)
|
||||||
|
expect(res.success).toEqual(true)
|
||||||
|
expect(workerRequests.sendSmtpEmail).toHaveBeenCalledTimes(1)
|
||||||
|
expect(workerRequests.sendSmtpEmail).toHaveBeenCalledWith({
|
||||||
|
to: "user1@test.com",
|
||||||
|
from: "admin@test.com",
|
||||||
|
subject: "hello",
|
||||||
|
contents: "testing",
|
||||||
|
cc: "cc",
|
||||||
|
bcc: "bcc",
|
||||||
|
invite,
|
||||||
|
automation: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -98,7 +98,8 @@ const environment = {
|
||||||
isInThread: () => {
|
isInThread: () => {
|
||||||
return process.env.FORKED_PROCESS
|
return process.env.FORKED_PROCESS
|
||||||
},
|
},
|
||||||
TOP_LEVEL_PATH: process.env.TOP_LEVEL_PATH,
|
TOP_LEVEL_PATH:
|
||||||
|
process.env.TOP_LEVEL_PATH || process.env.SERVER_TOP_LEVEL_PATH,
|
||||||
}
|
}
|
||||||
|
|
||||||
// threading can cause memory issues with node-ts in development
|
// threading can cause memory issues with node-ts in development
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import {
|
import {
|
||||||
ConnectionInfo,
|
ConnectionInfo,
|
||||||
|
Datasource,
|
||||||
DatasourceFeature,
|
DatasourceFeature,
|
||||||
DatasourceFieldType,
|
DatasourceFieldType,
|
||||||
DatasourcePlus,
|
DatasourcePlus,
|
||||||
|
@ -19,13 +20,15 @@ import { OAuth2Client } from "google-auth-library"
|
||||||
import { buildExternalTableId, finaliseExternalTables } from "./utils"
|
import { buildExternalTableId, finaliseExternalTables } from "./utils"
|
||||||
import { GoogleSpreadsheet, GoogleSpreadsheetRow } from "google-spreadsheet"
|
import { GoogleSpreadsheet, GoogleSpreadsheetRow } from "google-spreadsheet"
|
||||||
import fetch from "node-fetch"
|
import fetch from "node-fetch"
|
||||||
import { configs, HTTPError } from "@budibase/backend-core"
|
import { cache, configs, context, HTTPError } from "@budibase/backend-core"
|
||||||
import { dataFilters } from "@budibase/shared-core"
|
import { dataFilters } from "@budibase/shared-core"
|
||||||
import { GOOGLE_SHEETS_PRIMARY_KEY } from "../constants"
|
import { GOOGLE_SHEETS_PRIMARY_KEY } from "../constants"
|
||||||
|
import sdk from "../sdk"
|
||||||
|
|
||||||
interface GoogleSheetsConfig {
|
interface GoogleSheetsConfig {
|
||||||
spreadsheetId: string
|
spreadsheetId: string
|
||||||
auth: OAuthClientConfig
|
auth: OAuthClientConfig
|
||||||
|
continueSetupId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OAuthClientConfig {
|
interface OAuthClientConfig {
|
||||||
|
@ -72,7 +75,7 @@ const SCHEMA: Integration = {
|
||||||
},
|
},
|
||||||
datasource: {
|
datasource: {
|
||||||
spreadsheetId: {
|
spreadsheetId: {
|
||||||
display: "Google Sheet URL",
|
display: "Spreadsheet URL",
|
||||||
type: DatasourceFieldType.STRING,
|
type: DatasourceFieldType.STRING,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
@ -147,6 +150,7 @@ class GoogleSheetsIntegration implements DatasourcePlus {
|
||||||
|
|
||||||
async testConnection(): Promise<ConnectionInfo> {
|
async testConnection(): Promise<ConnectionInfo> {
|
||||||
try {
|
try {
|
||||||
|
await setupCreationAuth(this.config)
|
||||||
await this.connect()
|
await this.connect()
|
||||||
return { connected: true }
|
return { connected: true }
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
@ -566,6 +570,18 @@ class GoogleSheetsIntegration implements DatasourcePlus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function setupCreationAuth(datasouce: GoogleSheetsConfig) {
|
||||||
|
if (datasouce.continueSetupId) {
|
||||||
|
const appId = context.getAppId()
|
||||||
|
const tokens = await cache.get(
|
||||||
|
`datasource:creation:${appId}:google:${datasouce.continueSetupId}`
|
||||||
|
)
|
||||||
|
|
||||||
|
datasouce.auth = tokens.tokens
|
||||||
|
delete datasouce.continueSetupId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
schema: SCHEMA,
|
schema: SCHEMA,
|
||||||
integration: GoogleSheetsIntegration,
|
integration: GoogleSheetsIntegration,
|
||||||
|
|
|
@ -38,6 +38,9 @@ const SCHEMA: Integration = {
|
||||||
type: "password",
|
type: "password",
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
role: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
warehouse: {
|
warehouse: {
|
||||||
type: "string",
|
type: "string",
|
||||||
required: true,
|
required: true,
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
InternalTables,
|
InternalTables,
|
||||||
} from "../../db/utils"
|
} from "../../db/utils"
|
||||||
import { isEqual } from "lodash"
|
import { isEqual } from "lodash"
|
||||||
import { ContextUser, UserMetadata, User } from "@budibase/types"
|
import { ContextUser, UserMetadata, User, Database } from "@budibase/types"
|
||||||
|
|
||||||
export function combineMetadataAndUser(
|
export function combineMetadataAndUser(
|
||||||
user: ContextUser,
|
user: ContextUser,
|
||||||
|
@ -51,8 +51,10 @@ export function combineMetadataAndUser(
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function rawUserMetadata() {
|
export async function rawUserMetadata(db?: Database) {
|
||||||
const db = context.getAppDB()
|
if (!db) {
|
||||||
|
db = context.getAppDB()
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
await db.allDocs(
|
await db.allDocs(
|
||||||
getUserMetadataParams(null, {
|
getUserMetadataParams(null, {
|
||||||
|
@ -64,30 +66,36 @@ export async function rawUserMetadata() {
|
||||||
|
|
||||||
export async function syncGlobalUsers() {
|
export async function syncGlobalUsers() {
|
||||||
// sync user metadata
|
// sync user metadata
|
||||||
const db = context.getAppDB()
|
const dbs = [context.getDevAppDB(), context.getProdAppDB()]
|
||||||
const resp = await Promise.all([getGlobalUsers(), rawUserMetadata()])
|
for (let db of dbs) {
|
||||||
const users = resp[0] as User[]
|
if (!(await db.exists())) {
|
||||||
const metadata = resp[1] as UserMetadata[]
|
|
||||||
const toWrite = []
|
|
||||||
for (let user of users) {
|
|
||||||
const combined = combineMetadataAndUser(user, metadata)
|
|
||||||
if (combined) {
|
|
||||||
toWrite.push(combined)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let foundEmails: string[] = []
|
|
||||||
for (let data of metadata) {
|
|
||||||
if (!data._id) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const alreadyExisting = data.email && foundEmails.indexOf(data.email) !== -1
|
const resp = await Promise.all([getGlobalUsers(), rawUserMetadata(db)])
|
||||||
const globalId = getGlobalIDFromUserMetadataID(data._id)
|
const users = resp[0] as User[]
|
||||||
if (!users.find(user => user._id === globalId) || alreadyExisting) {
|
const metadata = resp[1] as UserMetadata[]
|
||||||
toWrite.push({ ...data, _deleted: true })
|
const toWrite = []
|
||||||
|
for (let user of users) {
|
||||||
|
const combined = combineMetadataAndUser(user, metadata)
|
||||||
|
if (combined) {
|
||||||
|
toWrite.push(combined)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (data.email) {
|
let foundEmails: string[] = []
|
||||||
foundEmails.push(data.email)
|
for (let data of metadata) {
|
||||||
|
if (!data._id) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const alreadyExisting =
|
||||||
|
data.email && foundEmails.indexOf(data.email) !== -1
|
||||||
|
const globalId = getGlobalIDFromUserMetadataID(data._id)
|
||||||
|
if (!users.find(user => user._id === globalId) || alreadyExisting) {
|
||||||
|
toWrite.push({ ...data, _deleted: true })
|
||||||
|
}
|
||||||
|
if (data.email) {
|
||||||
|
foundEmails.push(data.email)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
await db.bulkDocs(toWrite)
|
||||||
}
|
}
|
||||||
await db.bulkDocs(toWrite)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { PathLike } from "fs"
|
import { PathLike } from "fs"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import { budibaseTempDir } from "../budibaseDir"
|
import { budibaseTempDir } from "../budibaseDir"
|
||||||
import { join } from "path"
|
import { resolve, join } from "path"
|
||||||
import env from "../../environment"
|
import env from "../../environment"
|
||||||
import tar from "tar"
|
import tar from "tar"
|
||||||
import environment from "../../environment"
|
import environment from "../../environment"
|
||||||
const uuid = require("uuid/v4")
|
const uuid = require("uuid/v4")
|
||||||
|
|
||||||
export const TOP_LEVEL_PATH =
|
export const TOP_LEVEL_PATH =
|
||||||
environment.TOP_LEVEL_PATH || join(__dirname, "..", "..", "..")
|
environment.TOP_LEVEL_PATH || resolve(join(__dirname, "..", "..", ".."))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Upon first startup of instance there may not be everything we need in tmp directory, set it up.
|
* Upon first startup of instance there may not be everything we need in tmp directory, set it up.
|
||||||
|
|
|
@ -122,11 +122,8 @@ export async function getGlobalUsers(
|
||||||
delete user.forceResetPassword
|
delete user.forceResetPassword
|
||||||
return user
|
return user
|
||||||
})
|
})
|
||||||
if (!appId) {
|
|
||||||
return globalUsers
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts?.noProcessing) {
|
if (opts?.noProcessing || !appId) {
|
||||||
return globalUsers
|
return globalUsers
|
||||||
} else {
|
} else {
|
||||||
// pass in the groups, meaning we don't actually need to retrieve them for
|
// pass in the groups, meaning we don't actually need to retrieve them for
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {
|
||||||
env as coreEnv,
|
env as coreEnv,
|
||||||
} from "@budibase/backend-core"
|
} from "@budibase/backend-core"
|
||||||
import { updateAppRole } from "./global"
|
import { updateAppRole } from "./global"
|
||||||
import { BBContext, User } from "@budibase/types"
|
import { BBContext, User, EmailInvite } from "@budibase/types"
|
||||||
|
|
||||||
export function request(ctx?: BBContext, request?: any) {
|
export function request(ctx?: BBContext, request?: any) {
|
||||||
if (!request.headers) {
|
if (!request.headers) {
|
||||||
|
@ -65,15 +65,25 @@ async function checkResponse(
|
||||||
}
|
}
|
||||||
|
|
||||||
// have to pass in the tenant ID as this could be coming from an automation
|
// have to pass in the tenant ID as this could be coming from an automation
|
||||||
export async function sendSmtpEmail(
|
export async function sendSmtpEmail({
|
||||||
to: string,
|
to,
|
||||||
from: string,
|
from,
|
||||||
subject: string,
|
subject,
|
||||||
contents: string,
|
contents,
|
||||||
cc: string,
|
cc,
|
||||||
bcc: string,
|
bcc,
|
||||||
|
automation,
|
||||||
|
invite,
|
||||||
|
}: {
|
||||||
|
to: string
|
||||||
|
from: string
|
||||||
|
subject: string
|
||||||
|
contents: string
|
||||||
|
cc: string
|
||||||
|
bcc: string
|
||||||
automation: boolean
|
automation: boolean
|
||||||
) {
|
invite?: EmailInvite
|
||||||
|
}) {
|
||||||
// tenant ID will be set in header
|
// tenant ID will be set in header
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
checkSlashesInUrl(env.WORKER_URL + `/api/global/email/send`),
|
checkSlashesInUrl(env.WORKER_URL + `/api/global/email/send`),
|
||||||
|
@ -88,6 +98,7 @@ export async function sendSmtpEmail(
|
||||||
bcc,
|
bcc,
|
||||||
purpose: "custom",
|
purpose: "custom",
|
||||||
automation,
|
automation,
|
||||||
|
invite,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import authorized from "../middleware/authorized"
|
import authorized from "../middleware/authorized"
|
||||||
import { BaseSocket } from "./websocket"
|
import { BaseSocket } from "./websocket"
|
||||||
import { permissions } from "@budibase/backend-core"
|
import { permissions, events } from "@budibase/backend-core"
|
||||||
import http from "http"
|
import http from "http"
|
||||||
import Koa from "koa"
|
import Koa from "koa"
|
||||||
import { Datasource, Table, SocketSession, ContextUser } from "@budibase/types"
|
import { Datasource, Table, SocketSession, ContextUser } from "@budibase/types"
|
||||||
|
@ -22,6 +22,9 @@ export default class BuilderSocket extends BaseSocket {
|
||||||
// Reply with all users in current room
|
// Reply with all users in current room
|
||||||
const sessions = await this.getRoomSessions(appId)
|
const sessions = await this.getRoomSessions(appId)
|
||||||
callback({ users: sessions })
|
callback({ users: sessions })
|
||||||
|
|
||||||
|
// Track usage
|
||||||
|
await events.user.dataCollaboration(sessions.length)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Document } from "../document"
|
import { Document } from "../document"
|
||||||
import { EventEmitter } from "events"
|
import { EventEmitter } from "events"
|
||||||
|
import { User } from "../global"
|
||||||
|
|
||||||
export enum AutomationIOType {
|
export enum AutomationIOType {
|
||||||
OBJECT = "object",
|
OBJECT = "object",
|
||||||
|
@ -8,6 +9,7 @@ export enum AutomationIOType {
|
||||||
NUMBER = "number",
|
NUMBER = "number",
|
||||||
ARRAY = "array",
|
ARRAY = "array",
|
||||||
JSON = "json",
|
JSON = "json",
|
||||||
|
DATE = "date",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AutomationCustomIOType {
|
export enum AutomationCustomIOType {
|
||||||
|
@ -66,6 +68,33 @@ export enum AutomationActionStepId {
|
||||||
integromat = "integromat",
|
integromat = "integromat",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EmailInvite {
|
||||||
|
startTime: Date
|
||||||
|
endTime: Date
|
||||||
|
summary: string
|
||||||
|
location?: string
|
||||||
|
url?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SendEmailOpts {
|
||||||
|
// workspaceId If finer grain controls being used then this will lookup config for workspace.
|
||||||
|
workspaceId?: string
|
||||||
|
// user If sending to an existing user the object can be provided, this is used in the context.
|
||||||
|
user: User
|
||||||
|
// from If sending from an address that is not what is configured in the SMTP config.
|
||||||
|
from?: string
|
||||||
|
// contents If sending a custom email then can supply contents which will be added to it.
|
||||||
|
contents?: string
|
||||||
|
// subject A custom subject can be specified if the config one is not desired.
|
||||||
|
subject?: string
|
||||||
|
// info Pass in a structure of information to be stored alongside the invitation.
|
||||||
|
info?: any
|
||||||
|
cc?: boolean
|
||||||
|
bcc?: boolean
|
||||||
|
automation?: boolean
|
||||||
|
invite?: EmailInvite
|
||||||
|
}
|
||||||
|
|
||||||
export const AutomationStepIdArray = [
|
export const AutomationStepIdArray = [
|
||||||
...Object.values(AutomationActionStepId),
|
...Object.values(AutomationActionStepId),
|
||||||
...Object.values(AutomationTriggerStepId),
|
...Object.values(AutomationTriggerStepId),
|
||||||
|
@ -90,6 +119,7 @@ interface BaseIOStructure {
|
||||||
customType?: AutomationCustomIOType
|
customType?: AutomationCustomIOType
|
||||||
title?: string
|
title?: string
|
||||||
description?: string
|
description?: string
|
||||||
|
dependsOn?: string
|
||||||
enum?: string[]
|
enum?: string[]
|
||||||
pretty?: string[]
|
pretty?: string[]
|
||||||
properties?: {
|
properties?: {
|
||||||
|
|
|
@ -26,6 +26,9 @@ export enum Event {
|
||||||
USER_PASSWORD_RESET_REQUESTED = "user:password:reset:requested",
|
USER_PASSWORD_RESET_REQUESTED = "user:password:reset:requested",
|
||||||
USER_PASSWORD_RESET = "user:password:reset",
|
USER_PASSWORD_RESET = "user:password:reset",
|
||||||
|
|
||||||
|
// USER / COLLABORATION
|
||||||
|
USER_DATA_COLLABORATION = "user:data:collaboration",
|
||||||
|
|
||||||
// EMAIL
|
// EMAIL
|
||||||
EMAIL_SMTP_CREATED = "email:smtp:created",
|
EMAIL_SMTP_CREATED = "email:smtp:created",
|
||||||
EMAIL_SMTP_UPDATED = "email:smtp:updated",
|
EMAIL_SMTP_UPDATED = "email:smtp:updated",
|
||||||
|
@ -233,6 +236,7 @@ export const AuditedEventFriendlyName: Record<Event, string | undefined> = {
|
||||||
[Event.USER_PASSWORD_FORCE_RESET]: undefined,
|
[Event.USER_PASSWORD_FORCE_RESET]: undefined,
|
||||||
[Event.USER_GROUP_ONBOARDING]: undefined,
|
[Event.USER_GROUP_ONBOARDING]: undefined,
|
||||||
[Event.USER_ONBOARDING_COMPLETE]: undefined,
|
[Event.USER_ONBOARDING_COMPLETE]: undefined,
|
||||||
|
[Event.USER_DATA_COLLABORATION]: undefined,
|
||||||
|
|
||||||
// EMAIL
|
// EMAIL
|
||||||
[Event.EMAIL_SMTP_CREATED]: `Email configuration created`,
|
[Event.EMAIL_SMTP_CREATED]: `Email configuration created`,
|
||||||
|
|
|
@ -86,3 +86,7 @@ export interface UserPasswordResetEvent extends BaseEvent {
|
||||||
email: string
|
email: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserDataCollaborationEvent extends BaseEvent {
|
||||||
|
users: number
|
||||||
|
}
|
||||||
|
|
|
@ -13,7 +13,8 @@ RUN yarn global add pm2
|
||||||
|
|
||||||
|
|
||||||
COPY package.json .
|
COPY package.json .
|
||||||
RUN yarn install --frozen-lockfile --production=true
|
COPY dist/yarn.lock .
|
||||||
|
RUN yarn install --production=true
|
||||||
# Remove unneeded data from file system to reduce image size
|
# Remove unneeded data from file system to reduce image size
|
||||||
RUN apk del .gyp \
|
RUN apk del .gyp \
|
||||||
&& yarn cache clean
|
&& yarn cache clean
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
"run:docker": "node dist/index.js",
|
"run:docker": "node dist/index.js",
|
||||||
"debug": "yarn build && node --expose-gc --inspect=9223 dist/index.js",
|
"debug": "yarn build && node --expose-gc --inspect=9223 dist/index.js",
|
||||||
"run:docker:cluster": "pm2-runtime start pm2.config.js",
|
"run:docker:cluster": "pm2-runtime start pm2.config.js",
|
||||||
"predocker": "yarn build --configuration=production",
|
"predocker": "yarn build && cp ../../yarn.lock ./dist/",
|
||||||
"build:docker": "yarn predocker && docker build . -t worker-service --label version=$BUDIBASE_RELEASE_VERSION",
|
"build:docker": "yarn predocker && docker build . -t worker-service --label version=$BUDIBASE_RELEASE_VERSION",
|
||||||
"dev:stack:init": "node ./scripts/dev/manage.js init",
|
"dev:stack:init": "node ./scripts/dev/manage.js init",
|
||||||
"dev:builder": "npm run dev:stack:init && rimraf dist/ && nodemon",
|
"dev:builder": "npm run dev:stack:init && rimraf dist/ && nodemon",
|
||||||
|
@ -53,6 +53,7 @@
|
||||||
"elastic-apm-node": "3.38.0",
|
"elastic-apm-node": "3.38.0",
|
||||||
"global-agent": "3.0.0",
|
"global-agent": "3.0.0",
|
||||||
"got": "11.8.3",
|
"got": "11.8.3",
|
||||||
|
"ical-generator": "4.1.0",
|
||||||
"joi": "17.6.0",
|
"joi": "17.6.0",
|
||||||
"koa": "2.13.4",
|
"koa": "2.13.4",
|
||||||
"koa-body": "4.2.0",
|
"koa-body": "4.2.0",
|
||||||
|
|
|
@ -140,7 +140,6 @@ export const datasourcePreAuth = async (ctx: any, next: any) => {
|
||||||
{
|
{
|
||||||
provider,
|
provider,
|
||||||
appId: ctx.query.appId,
|
appId: ctx.query.appId,
|
||||||
datasourceId: ctx.query.datasourceId,
|
|
||||||
},
|
},
|
||||||
Cookie.DatasourceAuth
|
Cookie.DatasourceAuth
|
||||||
)
|
)
|
||||||
|
|
|
@ -14,6 +14,7 @@ export async function sendEmail(ctx: BBContext) {
|
||||||
cc,
|
cc,
|
||||||
bcc,
|
bcc,
|
||||||
automation,
|
automation,
|
||||||
|
invite,
|
||||||
} = ctx.request.body
|
} = ctx.request.body
|
||||||
let user
|
let user
|
||||||
if (userId) {
|
if (userId) {
|
||||||
|
@ -29,6 +30,7 @@ export async function sendEmail(ctx: BBContext) {
|
||||||
cc,
|
cc,
|
||||||
bcc,
|
bcc,
|
||||||
automation,
|
automation,
|
||||||
|
invite,
|
||||||
})
|
})
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
...response,
|
...response,
|
||||||
|
|
|
@ -4,28 +4,11 @@ import { getTemplateByPurpose, EmailTemplates } from "../constants/templates"
|
||||||
import { getSettingsTemplateContext } from "./templates"
|
import { getSettingsTemplateContext } from "./templates"
|
||||||
import { processString } from "@budibase/string-templates"
|
import { processString } from "@budibase/string-templates"
|
||||||
import { getResetPasswordCode, getInviteCode } from "./redis"
|
import { getResetPasswordCode, getInviteCode } from "./redis"
|
||||||
import { User, SMTPInnerConfig } from "@budibase/types"
|
import { User, SendEmailOpts, SMTPInnerConfig } from "@budibase/types"
|
||||||
import { configs } from "@budibase/backend-core"
|
import { configs } from "@budibase/backend-core"
|
||||||
|
import ical from "ical-generator"
|
||||||
const nodemailer = require("nodemailer")
|
const nodemailer = require("nodemailer")
|
||||||
|
|
||||||
type SendEmailOpts = {
|
|
||||||
// workspaceId If finer grain controls being used then this will lookup config for workspace.
|
|
||||||
workspaceId?: string
|
|
||||||
// user If sending to an existing user the object can be provided, this is used in the context.
|
|
||||||
user: User
|
|
||||||
// from If sending from an address that is not what is configured in the SMTP config.
|
|
||||||
from?: string
|
|
||||||
// contents If sending a custom email then can supply contents which will be added to it.
|
|
||||||
contents?: string
|
|
||||||
// subject A custom subject can be specified if the config one is not desired.
|
|
||||||
subject?: string
|
|
||||||
// info Pass in a structure of information to be stored alongside the invitation.
|
|
||||||
info?: any
|
|
||||||
cc?: boolean
|
|
||||||
bcc?: boolean
|
|
||||||
automation?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const TEST_MODE = env.ENABLE_EMAIL_TEST_MODE && env.isDev()
|
const TEST_MODE = env.ENABLE_EMAIL_TEST_MODE && env.isDev()
|
||||||
const TYPE = TemplateType.EMAIL
|
const TYPE = TemplateType.EMAIL
|
||||||
|
|
||||||
|
@ -200,6 +183,26 @@ export async function sendEmail(
|
||||||
context
|
context
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (opts?.invite) {
|
||||||
|
const calendar = ical({
|
||||||
|
name: "Invite",
|
||||||
|
})
|
||||||
|
calendar.createEvent({
|
||||||
|
start: opts.invite.startTime,
|
||||||
|
end: opts.invite.endTime,
|
||||||
|
summary: opts.invite.summary,
|
||||||
|
location: opts.invite.location,
|
||||||
|
url: opts.invite.url,
|
||||||
|
})
|
||||||
|
message = {
|
||||||
|
...message,
|
||||||
|
icalEvent: {
|
||||||
|
method: "request",
|
||||||
|
content: calendar.toString(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const response = await transport.sendMail(message)
|
const response = await transport.sendMail(message)
|
||||||
if (TEST_MODE) {
|
if (TEST_MODE) {
|
||||||
console.log("Test email URL: " + nodemailer.getTestMessageUrl(response))
|
console.log("Test email URL: " + nodemailer.getTestMessageUrl(response))
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
#!/bin/bash
|
||||||
|
echo "Resetting package versions"
|
||||||
|
yarn lerna exec "yarn version --no-git-tag-version --new-version=0.0.0"
|
||||||
|
echo "Updating dependencies"
|
||||||
|
node scripts/syncLocalDependencies.js "0.0.0"
|
||||||
|
git checkout package.json
|
||||||
|
echo "Package versions reset!"
|
|
@ -0,0 +1,12 @@
|
||||||
|
#!/bin/bash
|
||||||
|
version=$(cat lerna.json \
|
||||||
|
| grep version \
|
||||||
|
| head -1 \
|
||||||
|
| awk -F: '{gsub(/"/,"",$2);gsub(/[[:space:]]*/,"",$2); print $2}' \
|
||||||
|
| sed 's/[",]//g')
|
||||||
|
echo "Setting version $version"
|
||||||
|
yarn lerna exec "yarn version --no-git-tag-version --new-version=$version"
|
||||||
|
echo "Updating dependencies"
|
||||||
|
node scripts/syncLocalDependencies.js $version
|
||||||
|
echo "Syncing yarn workspace"
|
||||||
|
yarn
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
if [ -z "$1" ]
|
if [ -z "$1" ]
|
||||||
then
|
then
|
||||||
echo "Error: version number is required. Usage: $0 [major|minor|patch|prerelease]"
|
echo "Error: bump type is required. Usage: $0 [major|minor|patch|prerelease]"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -13668,6 +13668,13 @@ husky@^8.0.3:
|
||||||
resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.3.tgz#4936d7212e46d1dea28fef29bb3a108872cd9184"
|
resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.3.tgz#4936d7212e46d1dea28fef29bb3a108872cd9184"
|
||||||
integrity sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==
|
integrity sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==
|
||||||
|
|
||||||
|
ical-generator@4.1.0:
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/ical-generator/-/ical-generator-4.1.0.tgz#2a336c951864c5583a2aa715d16f2edcdfd2d90b"
|
||||||
|
integrity sha512-5GrFDJ8SAOj8cB9P1uEZIfKrNxSZ1R2eOQfZePL+CtdWh4RwNXWe8b0goajz+Hu37vcipG3RVldoa2j57Y20IA==
|
||||||
|
dependencies:
|
||||||
|
uuid-random "^1.3.2"
|
||||||
|
|
||||||
iconv-lite@0.4.24, iconv-lite@^0.4.15, iconv-lite@^0.4.24, iconv-lite@^0.4.5:
|
iconv-lite@0.4.24, iconv-lite@^0.4.15, iconv-lite@^0.4.24, iconv-lite@^0.4.5:
|
||||||
version "0.4.24"
|
version "0.4.24"
|
||||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||||
|
@ -25279,6 +25286,11 @@ utils-merge@1.0.1, utils-merge@1.x.x:
|
||||||
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
||||||
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
|
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
|
||||||
|
|
||||||
|
uuid-random@^1.3.2:
|
||||||
|
version "1.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/uuid-random/-/uuid-random-1.3.2.tgz#96715edbaef4e84b1dcf5024b00d16f30220e2d0"
|
||||||
|
integrity sha512-UOzej0Le/UgkbWEO8flm+0y+G+ljUon1QWTEZOq1rnMAsxo2+SckbiZdKzAHHlVh6gJqI1TjC/xwgR50MuCrBQ==
|
||||||
|
|
||||||
uuid@3.3.2:
|
uuid@3.3.2:
|
||||||
version "3.3.2"
|
version "3.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
|
||||||
|
|
Loading…
Reference in New Issue