This commit is contained in:
Martin McKeaveney 2023-05-24 11:25:49 +01:00
commit bbab2a9adc
286 changed files with 11545 additions and 1821 deletions

View File

@ -22,42 +22,45 @@ jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Use Node.js 14.x - name: Use Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v3
with: with:
node-version: 14.x node-version: 14.x
cache: "yarn"
- run: yarn - run: yarn
- run: yarn lint - run: yarn lint
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
with:
submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- name: Use Node.js 14.x - name: Use Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v3
with: with:
node-version: 14.x node-version: 14.x
- name: Install Pro cache: "yarn"
run: yarn install:pro $BRANCH $BASE_BRANCH
- run: yarn - run: yarn
- run: yarn bootstrap
- run: yarn build - run: yarn build
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
with:
submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- name: Use Node.js 14.x - name: Use Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v3
with: with:
node-version: 14.x node-version: 14.x
- name: Install Pro cache: "yarn"
run: yarn install:pro $BRANCH $BASE_BRANCH
- run: yarn - run: yarn
- run: yarn bootstrap - run: yarn build --scope=@budibase/types --scope=@budibase/shared-core --scope=@budibase/string-templates
- run: yarn build - run: yarn test --ignore=@budibase/pro
- run: yarn test
- uses: codecov/codecov-action@v3 - uses: codecov/codecov-action@v3
with: with:
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
@ -67,32 +70,58 @@ jobs:
test-pro: test-pro:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
with:
submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- name: Use Node.js 14.x - name: Use Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v3
with: with:
node-version: 14.x node-version: 14.x
- name: Install Pro cache: "yarn"
run: yarn install:pro $BRANCH $BASE_BRANCH
- run: yarn - run: yarn
- run: yarn bootstrap - run: yarn test --scope=@budibase/pro
- run: yarn test:pro
integration-test: integration-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
with:
submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- name: Use Node.js 14.x - name: Use Node.js 14.x
uses: actions/setup-node@v1 uses: actions/setup-node@v3
with: with:
node-version: 14.x node-version: 14.x
- name: Install Pro cache: "yarn"
run: yarn install:pro $BRANCH $BASE_BRANCH - run: yarn
- run: yarn && yarn bootstrap && yarn build - run: yarn build
- run: | - name: Run tests
run: |
cd qa-core cd qa-core
yarn setup yarn setup
yarn test:ci yarn test:ci
env: env:
BB_ADMIN_USER_EMAIL: admin BB_ADMIN_USER_EMAIL: admin
BB_ADMIN_USER_PASSWORD: admin BB_ADMIN_USER_PASSWORD: admin
check-pro-submodule:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
fetch-depth: 0
- name: Check submodule
run: |
cd packages/pro
git fetch
if ! git merge-base --is-ancestor $(git log -n 1 --pretty=format:%H) origin/develop; then
echo "Current commit has not been merged to develop"
echo "Refer to the pro repo to merge your changes: https://github.com/Budibase/budibase-pro/blob/develop/docs/getting_started.md"
exit 1
else
echo "All good, the submodule had been merged!"
fi

View File

@ -3,18 +3,8 @@ concurrency: release-prerelease
on: on:
push: push:
branches: tags:
- develop - v*-alpha.*
paths:
- '.aws/**'
- '.github/**'
- 'charts/**'
- 'packages/**'
- 'scripts/**'
- 'package.json'
- 'yarn.lock'
- 'package.json'
- 'yarn.lock'
workflow_dispatch: workflow_dispatch:
env: env:
@ -30,24 +20,39 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Fail if branch is not develop
if: github.ref != 'refs/heads/develop'
run: |
echo "Ref is not develop, you must run this job from develop."
exit 1
- 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 develop
run: |
if ! git merge-base --is-ancestor ${{ github.sha }} origin/develop; then
echo "Tag is not in develop"
exit 1
fi
- uses: actions/setup-node@v1 - uses: actions/setup-node@v1
with: with:
node-version: 14.x node-version: 14.x
- name: Install Pro
run: yarn install:pro develop
- run: yarn - run: yarn
- run: yarn bootstrap - name: Update versions
run: |
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 build - run: yarn build
- run: yarn build:sdk - run: yarn build:sdk
# - run: yarn test
- name: Publish budibase packages to NPM - name: Publish budibase packages to NPM
env: env:
@ -56,6 +61,8 @@ jobs:
# 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 "<>"
git submodule foreach git commit -a -m 'Release process'
git commit -a -m 'Release process'
echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc
yarn release:develop yarn release:develop
@ -84,7 +91,7 @@ jobs:
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 0.0.0-develop --app-version develop --destination sync helm package charts/budibase --version 0.0.0-develop --app-version develop --destination sync

View File

@ -3,29 +3,16 @@ concurrency: release
on: on:
push: push:
branches: tags:
- master - "v[0-9]+.[0-9]+.[0-9]+"
paths: # Exclude all pre-releases
- '.aws/**' - "!v*[0-9]+.[0-9]+.[0-9]+-*"
- '.github/**'
- 'charts/**'
- 'packages/**'
- 'scripts/**'
- 'package.json'
- 'yarn.lock'
- 'package.json'
- 'yarn.lock'
workflow_dispatch: workflow_dispatch:
inputs: inputs:
versioning: tags:
type: choice description: "Release tag"
description: "Versioning type: patch, minor, major"
default: patch
options:
- patch
- minor
- major
required: true required: true
type: boolean
env: env:
# Posthog token used by ui at build time # Posthog token used by ui at build time
@ -38,21 +25,37 @@ jobs:
release-images: release-images:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2
with:
submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
fetch-depth: 0
- name: Fail if branch is not master - name: Fail if branch is not master
if: github.ref != 'refs/heads/master' if: github.ref != 'refs/heads/master'
run: | run: |
echo "Ref is not master, you must run this job from master." echo "Ref is not master, you must run this job from master."
exit 1 // Change to "exit 1" when merged. Left to 0 to not fail all the pipelines and not to cause noise
- uses: actions/checkout@v2 exit 0
- uses: actions/setup-node@v1 - uses: actions/setup-node@v1
with: with:
node-version: 14.x node-version: 14.x
- name: Install Pro
run: yarn install:pro master
- run: yarn - run: yarn
- run: yarn bootstrap - name: Update versions
run: |
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 - run: yarn build
- run: yarn build:sdk - run: yarn build:sdk
@ -65,10 +68,12 @@ jobs:
# 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"
git config --global user.email "<>" git config --global user.email "<>"
git submodule foreach git commit -a -m 'Release process'
git commit -a -m 'Release process'
echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc
yarn release yarn release
- name: 'Get Previous tag' - name: "Get Previous tag"
id: previoustag id: previoustag
uses: "WyriHaximus/github-action-get-previous-tag@v1" uses: "WyriHaximus/github-action-get-previous-tag@v1"
@ -103,7 +108,7 @@ jobs:
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 0.0.0-master --app-version v"$RELEASE_VERSION" --destination sync helm package charts/budibase --version 0.0.0-master --app-version v"$RELEASE_VERSION" --destination sync

39
.github/workflows/tag-prerelease.yml vendored Normal file
View File

@ -0,0 +1,39 @@
name: Tag prerelease
concurrency: release-prerelease
on:
push:
branches:
- develop
paths:
- ".aws/**"
- ".github/**"
- "charts/**"
- "packages/**"
- "scripts/**"
- "package.json"
- "yarn.lock"
workflow_dispatch:
jobs:
tag-prerelease:
runs-on: ubuntu-latest
steps:
- name: Fail if branch is not develop
if: github.ref != 'refs/heads/develop'
run: |
echo "Ref is not develop, you must run this job from develop."
exit 1
- uses: actions/checkout@v2
with:
submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- run: yarn
- name: Tag prerelease
run: |
# setup the username and email.
git config --global user.name "Budibase Staging Release Bot"
git config --global user.email "<>"
./scripts/versionCommit.sh prerelease

49
.github/workflows/tag-release.yml vendored Normal file
View File

@ -0,0 +1,49 @@
name: Tag release
concurrency: release-prerelease
on:
push:
branches:
- master
paths:
- ".aws/**"
- ".github/**"
- "charts/**"
- "packages/**"
- "scripts/**"
- "package.json"
- "yarn.lock"
workflow_dispatch:
inputs:
versioning:
type: choice
description: "Versioning type: patch, minor, major"
default: patch
options:
- patch
- minor
- major
required: true
jobs:
tag-prerelease:
runs-on: ubuntu-latest
steps:
- name: Fail if branch is not master
if: github.ref != 'refs/heads/master'
run: |
echo "Ref is not master, you must run this job from master."
exit 1
- uses: actions/checkout@v2
with:
submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- run: yarn
- name: Tag prerelease
run: |
# setup the username and email.
git config --global user.name "Budibase Staging Release Bot"
git config --global user.email "<>"
./scripts/versionCommit.sh ${{ github.event.inputs.versioning }}

3
.gitmodules vendored
View File

@ -0,0 +1,3 @@
[submodule "packages/pro"]
path = packages/pro
url = git@github.com:Budibase/budibase-pro.git

4
.husky/post-checkout Executable file
View File

@ -0,0 +1,4 @@
# .husky/post-checkout
# ...
git config submodule.recurse true

View File

@ -1,13 +1,17 @@
## Dev Environment on Debian 11 ## Dev Environment on Debian 11
### Install NVM & Node 14 ### Install NVM & Node 14
NVM documentation: https://github.com/nvm-sh/nvm#installing-and-updating NVM documentation: https://github.com/nvm-sh/nvm#installing-and-updating
Install NVM Install NVM
``` ```
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
``` ```
Install Node 14 Install Node 14
``` ```
nvm install 14 nvm install 14
``` ```
@ -17,13 +21,16 @@ nvm install 14
``` ```
npm install -g yarn jest lerna npm install -g yarn jest lerna
``` ```
### Install Docker and Docker Compose ### Install Docker and Docker Compose
``` ```
apt install docker.io apt install docker.io
pip3 install docker-compose pip3 install docker-compose
``` ```
### Clone the repo ### Clone the repo
``` ```
git clone https://github.com/Budibase/budibase.git git clone https://github.com/Budibase/budibase.git
``` ```
@ -44,10 +51,13 @@ This setup process was tested on Debian 11 (bullseye) with version numbers show
cd budibase cd budibase
yarn setup yarn setup
``` ```
The yarn setup command runs several build steps i.e. The yarn setup command runs several build steps i.e.
``` ```
node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev
``` ```
So this command will actually run the application in dev mode. It creates .env files under `./packages/server` and `./packages/worker` and runs docker containers for each service via docker-compose. So this command will actually run the application in dev mode. It creates .env files under `./packages/server` and `./packages/worker` and runs docker containers for each service via docker-compose.
The dev version will be available on port 10000 i.e. The dev version will be available on port 10000 i.e.
@ -55,6 +65,7 @@ The dev version will be available on port 10000 i.e.
http://127.0.0.1:10000/builder/admin http://127.0.0.1:10000/builder/admin
### File descriptor issues with Vite and Chrome in Linux ### File descriptor issues with Vite and Chrome in Linux
If your dev environment stalls forever, with some network requests stuck in flight, it's likely that Chrome is trying to open more file descriptors than your system allows. If your dev environment stalls forever, with some network requests stuck in flight, it's likely that Chrome is trying to open more file descriptors than your system allows.
To fix this, apply the following tweaks. To fix this, apply the following tweaks.

View File

@ -8,10 +8,10 @@ Install instructions [here](https://brew.sh/)
`eval $(/opt/homebrew/bin/brew shellenv)` line to your `.zshrc`. This will make your zsh to find the apps you install `eval $(/opt/homebrew/bin/brew shellenv)` line to your `.zshrc`. This will make your zsh to find the apps you install
through brew. through brew.
### Install Node ### Install Node
Budibase requires a recent version of node 14: Budibase requires a recent version of node 14:
``` ```
brew install node npm brew install node npm
node -v node -v
@ -22,12 +22,15 @@ node -v
``` ```
npm install -g yarn jest lerna npm install -g yarn jest lerna
``` ```
### Install Docker and Docker Compose ### Install Docker and Docker Compose
``` ```
brew install docker docker-compose brew install docker docker-compose
``` ```
### Clone the repo ### Clone the repo
``` ```
git clone https://github.com/Budibase/budibase.git git clone https://github.com/Budibase/budibase.git
``` ```
@ -48,10 +51,13 @@ This setup process was tested on Mac OSX 12 (Monterey) with version numbers show
cd budibase cd budibase
yarn setup yarn setup
``` ```
The yarn setup command runs several build steps i.e. The yarn setup command runs several build steps i.e.
``` ```
node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev
``` ```
So this command will actually run the application in dev mode. It creates .env files under `./packages/server` and `./packages/worker` and runs docker containers for each service via docker-compose. So this command will actually run the application in dev mode. It creates .env files under `./packages/server` and `./packages/worker` and runs docker containers for each service via docker-compose.
The dev version will be available on port 10000 i.e. The dev version will be available on port 10000 i.e.

View File

@ -1,13 +1,15 @@
## Dev Environment on Windows 10/11 (WSL2) ## Dev Environment on Windows 10/11 (WSL2)
### Install WSL with Ubuntu LTS ### Install WSL with Ubuntu LTS
Enable WSL 2 on Windows 10/11 for docker support. Enable WSL 2 on Windows 10/11 for docker support.
``` ```
wsl --set-default-version 2 wsl --set-default-version 2
``` ```
Install Ubuntu LTS. Install Ubuntu LTS.
``` ```
wsl --install Ubuntu wsl --install Ubuntu
``` ```
@ -16,6 +18,7 @@ Or follow the instruction here:
https://learn.microsoft.com/en-us/windows/wsl/install https://learn.microsoft.com/en-us/windows/wsl/install
### Install Docker in windows ### Install Docker in windows
Download the installer from docker and install it. Download the installer from docker and install it.
Check this url for more detailed instructions: Check this url for more detailed instructions:
@ -24,18 +27,21 @@ https://docs.docker.com/desktop/install/windows-install/
You should follow the next steps from within the Ubuntu terminal. You should follow the next steps from within the Ubuntu terminal.
### Install NVM & Node 14 ### Install NVM & Node 14
NVM documentation: https://github.com/nvm-sh/nvm#installing-and-updating NVM documentation: https://github.com/nvm-sh/nvm#installing-and-updating
Install NVM Install NVM
``` ```
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
``` ```
Install Node 14 Install Node 14
``` ```
nvm install 14 nvm install 14
``` ```
### Install npm requirements ### Install npm requirements
``` ```
@ -43,6 +49,7 @@ npm install -g yarn jest lerna
``` ```
### Clone the repo ### Clone the repo
``` ```
git clone https://github.com/Budibase/budibase.git git clone https://github.com/Budibase/budibase.git
``` ```
@ -63,10 +70,13 @@ This setup process was tested on Windows 11 with version numbers show below. You
cd budibase cd budibase
yarn setup yarn setup
``` ```
The yarn setup command runs several build steps i.e. The yarn setup command runs several build steps i.e.
``` ```
node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev
``` ```
So this command will actually run the application in dev mode. It creates .env files under `./packages/server` and `./packages/worker` and runs docker containers for each service via docker-compose. So this command will actually run the application in dev mode. It creates .env files under `./packages/server` and `./packages/worker` and runs docker containers for each service via docker-compose.
The dev version will be available on port 10000 i.e. The dev version will be available on port 10000 i.e.
@ -74,6 +84,7 @@ The dev version will be available on port 10000 i.e.
http://127.0.0.1:10000/builder/admin http://127.0.0.1:10000/builder/admin
### Working with the code ### Working with the code
Here are the instructions to work on the application from within Visual Studio Code (in Windows) through the WSL. All the commands and files are within the Ubuntu system and it should run as if you were working on a Linux machine. Here are the instructions to work on the application from within Visual Studio Code (in Windows) through the WSL. All the commands and files are within the Ubuntu system and it should run as if you were working on a Linux machine.
https://code.visualstudio.com/docs/remote/wsl https://code.visualstudio.com/docs/remote/wsl

View File

@ -5,8 +5,11 @@ ENV COUCHDB_PASSWORD admin
EXPOSE 5984 EXPOSE 5984
RUN apt-get update && apt-get install -y --no-install-recommends software-properties-common wget unzip curl && \ RUN apt-get update && apt-get install -y --no-install-recommends software-properties-common wget unzip curl && \
apt-add-repository 'deb http://security.debian.org/debian-security stretch/updates main' && \ wget -qO - https://adoptopenjdk.jfrog.io/adoptopenjdk/api/gpg/key/public | apt-key add - && \
apt-get update && apt-get install -y --no-install-recommends openjdk-8-jre && \ apt-add-repository 'deb http://security.debian.org/debian-security bullseye-security/updates main' && \
apt-add-repository 'deb http://archive.debian.org/debian stretch-backports main' && \
apt-add-repository --yes https://adoptopenjdk.jfrog.io/adoptopenjdk/deb/ && \
apt-get update && apt-get install -y --no-install-recommends adoptopenjdk-8-hotspot && \
rm -rf /var/lib/apt/lists/ rm -rf /var/lib/apt/lists/
# setup clouseau # setup clouseau

View File

@ -55,7 +55,7 @@ http {
set $csp_style "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me https://maxcdn.bootstrapcdn.com"; set $csp_style "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me https://maxcdn.bootstrapcdn.com";
set $csp_object "object-src 'none'"; set $csp_object "object-src 'none'";
set $csp_base_uri "base-uri 'self'"; set $csp_base_uri "base-uri 'self'";
set $csp_connect "connect-src 'self' https://*.budibase.net https://api-iam.intercom.io https://api-iam.intercom.io https://api-ping.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io https://nexus-websocket-a.intercom.io https://nexus-websocket-b.intercom.io https://uploads.intercomcdn.com https://uploads.intercomusercontent.com https://*.s3.amazonaws.com https://*.s3.us-east-2.amazonaws.com https://*.s3.us-east-1.amazonaws.com https://*.s3.us-west-1.amazonaws.com https://*.s3.us-west-2.amazonaws.com https://*.s3.af-south-1.amazonaws.com https://*.s3.ap-east-1.amazonaws.com https://*.s3.ap-southeast-3.amazonaws.com https://*.s3.ap-south-1.amazonaws.com https://*.s3.ap-northeast-3.amazonaws.com https://*.s3.ap-northeast-2.amazonaws.com https://*.s3.ap-southeast-1.amazonaws.com https://*.s3.ap-southeast-2.amazonaws.com https://*.s3.ap-northeast-1.amazonaws.com https://*.s3.ca-central-1.amazonaws.com https://*.s3.cn-north-1.amazonaws.com https://*.s3.cn-northwest-1.amazonaws.com https://*.s3.eu-central-1.amazonaws.com https://*.s3.eu-west-1.amazonaws.com https://*.s3.eu-west-2.amazonaws.com https://*.s3.eu-south-1.amazonaws.com https://*.s3.eu-west-3.amazonaws.com https://*.s3.eu-north-1.amazonaws.com https://*.s3.sa-east-1.amazonaws.com https://*.s3.me-south-1.amazonaws.com https://*.s3.us-gov-east-1.amazonaws.com https://*.s3.us-gov-west-1.amazonaws.com https://api.github.com"; set $csp_connect "connect-src 'self' https://*.budibase.net https://api-iam.intercom.io https://api-iam.intercom.io https://api-ping.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io https://nexus-websocket-a.intercom.io https://nexus-websocket-b.intercom.io https://uploads.intercomcdn.com https://uploads.intercomusercontent.com https://*.amazonaws.com https://*.s3.amazonaws.com https://*.s3.us-east-2.amazonaws.com https://*.s3.us-east-1.amazonaws.com https://*.s3.us-west-1.amazonaws.com https://*.s3.us-west-2.amazonaws.com https://*.s3.af-south-1.amazonaws.com https://*.s3.ap-east-1.amazonaws.com https://*.s3.ap-southeast-3.amazonaws.com https://*.s3.ap-south-1.amazonaws.com https://*.s3.ap-northeast-3.amazonaws.com https://*.s3.ap-northeast-2.amazonaws.com https://*.s3.ap-southeast-1.amazonaws.com https://*.s3.ap-southeast-2.amazonaws.com https://*.s3.ap-northeast-1.amazonaws.com https://*.s3.ca-central-1.amazonaws.com https://*.s3.cn-north-1.amazonaws.com https://*.s3.cn-northwest-1.amazonaws.com https://*.s3.eu-central-1.amazonaws.com https://*.s3.eu-west-1.amazonaws.com https://*.s3.eu-west-2.amazonaws.com https://*.s3.eu-south-1.amazonaws.com https://*.s3.eu-west-3.amazonaws.com https://*.s3.eu-north-1.amazonaws.com https://*.s3.sa-east-1.amazonaws.com https://*.s3.me-south-1.amazonaws.com https://*.s3.us-gov-east-1.amazonaws.com https://*.s3.us-gov-west-1.amazonaws.com https://api.github.com";
set $csp_font "font-src 'self' data: https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me https://maxcdn.bootstrapcdn.com https://js.intercomcdn.com https://fonts.intercomcdn.com"; set $csp_font "font-src 'self' data: https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me https://maxcdn.bootstrapcdn.com https://js.intercomcdn.com https://fonts.intercomcdn.com";
set $csp_frame "frame-src 'self' https:"; set $csp_frame "frame-src 'self' https:";
set $csp_img "img-src http: https: data: blob:"; set $csp_img "img-src http: https: data: blob:";
@ -82,6 +82,12 @@ http {
set $couchdb ${COUCHDB_UPSTREAM_URL}; set $couchdb ${COUCHDB_UPSTREAM_URL};
set $watchtower ${WATCHTOWER_UPSTREAM_URL}; set $watchtower ${WATCHTOWER_UPSTREAM_URL};
location /health {
access_log off;
add_header 'Content-Type' 'application/json';
return 200 '{ "status": "OK" }';
}
location /app { location /app {
proxy_pass $apps; proxy_pass $apps;
} }
@ -222,9 +228,9 @@ http {
rewrite ^/files/signed/(.*)$ /$1 break; rewrite ^/files/signed/(.*)$ /$1 break;
} }
client_header_timeout 60; client_header_timeout 120;
client_body_timeout 60; client_body_timeout 120;
keepalive_timeout 60; keepalive_timeout 120;
# gzip # gzip
gzip on; gzip on;

View File

@ -22,7 +22,7 @@ FROM budibase/couchdb
ARG TARGETARCH ARG TARGETARCH
ENV TARGETARCH $TARGETARCH ENV TARGETARCH $TARGETARCH
#TARGETBUILD can be set to single (for single docker image) or aas (for azure app service) #TARGETBUILD can be set to single (for single docker image) or aas (for azure app service)
# e.g. docker build --build-arg TARGETBUILD=aas .... # e.g. docker build --build-arg TARGETBUILD=aas ....
ARG TARGETBUILD=single ARG TARGETBUILD=single
ENV TARGETBUILD $TARGETBUILD ENV TARGETBUILD $TARGETBUILD
@ -32,7 +32,7 @@ COPY --from=build /worker /worker
# install base dependencies # install base dependencies
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y --no-install-recommends software-properties-common nginx uuid-runtime redis-server && \ apt-get install -y --no-install-recommends software-properties-common nginx uuid-runtime redis-server && \
apt-add-repository 'deb http://security.debian.org/debian-security stretch/updates main' && \ apt-add-repository 'deb http://security.debian.org/debian-security bullseye-security/updates main' && \
apt-get update apt-get update
# install other dependencies, nodejs, oracle requirements, jdk8, redis, nginx # install other dependencies, nodejs, oracle requirements, jdk8, redis, nginx

View File

@ -1,8 +1,22 @@
{ {
"version": "2.5.6-alpha.30", "version": "2.6.19-alpha.4",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [
"packages/backend-core",
"packages/bbui",
"packages/builder",
"packages/cli",
"packages/client",
"packages/frontend-core",
"packages/sdk",
"packages/server",
"packages/shared-core",
"packages/string-templates",
"packages/types",
"packages/worker",
"packages/pro/packages/pro"
],
"useWorkspaces": true, "useWorkspaces": true,
"packages": ["packages/*"],
"command": { "command": {
"publish": { "publish": {
"ignoreChanges": [ "ignoreChanges": [

View File

@ -8,7 +8,7 @@
"eslint": "^7.28.0", "eslint": "^7.28.0",
"eslint-plugin-cypress": "^2.11.3", "eslint-plugin-cypress": "^2.11.3",
"eslint-plugin-svelte3": "^3.2.0", "eslint-plugin-svelte3": "^3.2.0",
"husky": "^7.0.1", "husky": "^8.0.3",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"kill-port": "^1.6.1", "kill-port": "^1.6.1",
"lerna": "^6.6.1", "lerna": "^6.6.1",
@ -17,22 +17,22 @@
"prettier-plugin-svelte": "^2.3.0", "prettier-plugin-svelte": "^2.3.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rollup-plugin-replace": "^2.2.0", "rollup-plugin-replace": "^2.2.0",
"semver": "^7.5.0",
"svelte": "^3.38.2", "svelte": "^3.38.2",
"typescript": "4.7.3" "typescript": "4.7.3"
}, },
"scripts": { "scripts": {
"setup": "node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev", "preinstall": "node scripts/syncProPackage.js",
"bootstrap": "lerna link && ./scripts/link-dependencies.sh", "setup": "git config submodule.recurse true && git submodule update && node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev",
"bootstrap": "./scripts/bootstrap.sh && lerna link && ./scripts/link-dependencies.sh",
"build": "lerna run --stream build", "build": "lerna run --stream build",
"build:dev": "lerna run --stream prebuild && tsc --build --watch --preserveWatchOutput", "build:dev": "lerna run --stream prebuild && yarn nx run-many --target=build --output-style=dynamic --watch --preserveWatchOutput",
"backend:bootstrap": "./scripts/scopeBackend.sh && yarn run bootstrap", "backend:bootstrap": "./scripts/scopeBackend.sh && yarn run bootstrap",
"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 && yarn release:pro", "release": "lerna publish ${RELEASE_VERSION_TYPE:-patch} --yes --force-publish --no-git-tag-version --no-push --no-git-reset",
"release:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop --exact && yarn release:pro:develop", "release:develop": "lerna publish from-package --yes --force-publish --dist-tag develop --exact --no-git-tag-version --no-push --no-git-reset",
"release:pro": "bash scripts/pro/release.sh",
"release:pro:develop": "bash scripts/pro/release.sh 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",
@ -46,7 +46,6 @@
"dev:server": "yarn run kill-server && lerna run --stream --parallel dev:builder --concurrency 1 --scope @budibase/backend-core --scope @budibase/worker --scope @budibase/server", "dev:server": "yarn run kill-server && lerna run --stream --parallel dev:builder --concurrency 1 --scope @budibase/backend-core --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",
"test": "lerna run --stream test --stream", "test": "lerna run --stream test --stream",
"test:pro": "bash scripts/pro/test.sh",
"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}\"",
"lint": "yarn run lint:eslint && yarn run lint:prettier", "lint": "yarn run lint:eslint && yarn run lint:prettier",
@ -82,12 +81,25 @@
"mode:account": "yarn mode:cloud && yarn env:account:enable", "mode:account": "yarn mode:cloud && yarn env:account:enable",
"security:audit": "node scripts/audit.js", "security:audit": "node scripts/audit.js",
"postinstall": "husky install", "postinstall": "husky install",
"install:pro": "bash scripts/pro/install.sh", "dep:clean": "yarn clean -y && yarn bootstrap",
"dep:clean": "yarn clean && yarn bootstrap" "submodules:load": "git submodule init && git submodule update && yarn && yarn bootstrap",
"submodules:unload": "git submodule deinit --all && yarn && yarn bootstrap"
}, },
"workspaces": { "workspaces": {
"packages": [ "packages": [
"packages/*" "packages/backend-core",
"packages/bbui",
"packages/builder",
"packages/cli",
"packages/client",
"packages/frontend-core",
"packages/sdk",
"packages/server",
"packages/shared-core",
"packages/string-templates",
"packages/types",
"packages/worker",
"packages/pro/packages/pro"
] ]
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/backend-core", "name": "@budibase/backend-core",
"version": "2.5.6-alpha.30", "version": "0.0.1",
"description": "Budibase backend core libraries used in server and worker", "description": "Budibase backend core libraries used in server and worker",
"main": "dist/src/index.js", "main": "dist/src/index.js",
"types": "dist/src/index.d.ts", "types": "dist/src/index.d.ts",
@ -15,8 +15,6 @@
"prebuild": "rimraf dist/", "prebuild": "rimraf dist/",
"prepack": "cp package.json dist", "prepack": "cp package.json dist",
"build": "tsc -p tsconfig.build.json", "build": "tsc -p tsconfig.build.json",
"build:pro": "../../scripts/pro/build.sh",
"postbuild": "yarn run build:pro",
"build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput", "build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput",
"test": "bash scripts/test.sh", "test": "bash scripts/test.sh",
"test:watch": "jest --watchAll" "test:watch": "jest --watchAll"
@ -24,7 +22,7 @@
"dependencies": { "dependencies": {
"@budibase/nano": "10.1.2", "@budibase/nano": "10.1.2",
"@budibase/pouchdb-replication-stream": "1.2.10", "@budibase/pouchdb-replication-stream": "1.2.10",
"@budibase/types": "2.5.6-alpha.30", "@budibase/types": "0.0.1",
"@shopify/jest-koa-mocks": "5.0.1", "@shopify/jest-koa-mocks": "5.0.1",
"@techpass/passport-openidconnect": "0.3.2", "@techpass/passport-openidconnect": "0.3.2",
"aws-cloudfront-sign": "2.2.0", "aws-cloudfront-sign": "2.2.0",

View File

@ -47,7 +47,7 @@ async function put(
type: LockType.TRY_ONCE, type: LockType.TRY_ONCE,
name: LockName.PERSIST_WRITETHROUGH, name: LockName.PERSIST_WRITETHROUGH,
resource: key, resource: key,
ttl: 1000, ttl: 15000,
}, },
async () => { async () => {
const writeDb = async (toWrite: any) => { const writeDb = async (toWrite: any) => {
@ -71,6 +71,7 @@ async function put(
} }
} }
) )
if (!lockResponse.executed) { if (!lockResponse.executed) {
logWarn(`Ignoring redlock conflict in write-through cache`) logWarn(`Ignoring redlock conflict in write-through cache`)
} }

View File

@ -21,7 +21,7 @@ export enum ViewName {
AUTOMATION_LOGS = "automation_logs", AUTOMATION_LOGS = "automation_logs",
ACCOUNT_BY_EMAIL = "account_by_email", ACCOUNT_BY_EMAIL = "account_by_email",
PLATFORM_USERS_LOWERCASE = "platform_users_lowercase", PLATFORM_USERS_LOWERCASE = "platform_users_lowercase",
USER_BY_GROUP = "by_group_user", USER_BY_GROUP = "user_by_group",
APP_BACKUP_BY_TRIGGER = "by_trigger", APP_BACKUP_BY_TRIGGER = "by_trigger",
} }

View File

@ -104,6 +104,22 @@ async function newContext(updates: ContextMap, task: any) {
return Context.run(context, task) return Context.run(context, task)
} }
export async function doInAutomationContext(params: {
appId: string
automationId: string
task: any
}): Promise<any> {
const tenantId = getTenantIDFromAppID(params.appId)
return newContext(
{
tenantId,
appId: params.appId,
automationId: params.automationId,
},
params.task
)
}
export async function doInContext(appId: string, task: any): Promise<any> { export async function doInContext(appId: string, task: any): Promise<any> {
const tenantId = getTenantIDFromAppID(appId) const tenantId = getTenantIDFromAppID(appId)
return newContext( return newContext(
@ -187,6 +203,11 @@ export function getTenantId(): string {
return tenantId return tenantId
} }
export function getAutomationId(): string | undefined {
const context = Context.get()
return context?.automationId
}
export function getAppId(): string | undefined { export function getAppId(): string | undefined {
const context = Context.get() const context = Context.get()
const foundId = context?.appId const foundId = context?.appId

View File

@ -7,4 +7,5 @@ export type ContextMap = {
identity?: IdentityContext identity?: IdentityContext
environmentVariables?: Record<string, string> environmentVariables?: Record<string, string>
isScim?: boolean isScim?: boolean
automationId?: string
} }

View File

@ -12,7 +12,7 @@ import {
isDocument, isDocument,
} from "@budibase/types" } from "@budibase/types"
import { getCouchInfo } from "./connections" import { getCouchInfo } from "./connections"
import { directCouchCall } from "./utils" import { directCouchUrlCall } from "./utils"
import { getPouchDB } from "./pouchDB" import { getPouchDB } from "./pouchDB"
import { WriteStream, ReadStream } from "fs" import { WriteStream, ReadStream } from "fs"
import { newid } from "../../docIds/newid" import { newid } from "../../docIds/newid"
@ -46,6 +46,8 @@ export class DatabaseImpl implements Database {
private readonly instanceNano?: Nano.ServerScope private readonly instanceNano?: Nano.ServerScope
private readonly pouchOpts: DatabaseOpts private readonly pouchOpts: DatabaseOpts
private readonly couchInfo = getCouchInfo()
constructor(dbName?: string, opts?: DatabaseOpts, connection?: string) { constructor(dbName?: string, opts?: DatabaseOpts, connection?: string) {
if (dbName == null) { if (dbName == null) {
throw new Error("Database name cannot be undefined.") throw new Error("Database name cannot be undefined.")
@ -53,8 +55,8 @@ export class DatabaseImpl implements Database {
this.name = dbName this.name = dbName
this.pouchOpts = opts || {} this.pouchOpts = opts || {}
if (connection) { if (connection) {
const couchInfo = getCouchInfo(connection) this.couchInfo = getCouchInfo(connection)
this.instanceNano = buildNano(couchInfo) this.instanceNano = buildNano(this.couchInfo)
} }
if (!DatabaseImpl.nano) { if (!DatabaseImpl.nano) {
DatabaseImpl.init() DatabaseImpl.init()
@ -67,7 +69,11 @@ export class DatabaseImpl implements Database {
} }
async exists() { async exists() {
let response = await directCouchCall(`/${this.name}`, "HEAD") const response = await directCouchUrlCall({
url: `${this.couchInfo.url}/${this.name}`,
method: "HEAD",
cookie: this.couchInfo.cookie,
})
return response.status === 200 return response.status === 200
} }

View File

@ -4,21 +4,21 @@ export const getCouchInfo = (connection?: string) => {
const urlInfo = getUrlInfo(connection) const urlInfo = getUrlInfo(connection)
let username let username
let password let password
if (env.COUCH_DB_USERNAME) { if (urlInfo.auth?.username) {
// set from env
username = env.COUCH_DB_USERNAME
} else if (urlInfo.auth.username) {
// set from url // set from url
username = urlInfo.auth.username username = urlInfo.auth.username
} else if (env.COUCH_DB_USERNAME) {
// set from env
username = env.COUCH_DB_USERNAME
} else if (!env.isTest()) { } else if (!env.isTest()) {
throw new Error("CouchDB username not set") throw new Error("CouchDB username not set")
} }
if (env.COUCH_DB_PASSWORD) { if (urlInfo.auth?.password) {
// set from env
password = env.COUCH_DB_PASSWORD
} else if (urlInfo.auth.password) {
// set from url // set from url
password = urlInfo.auth.password password = urlInfo.auth.password
} else if (env.COUCH_DB_PASSWORD) {
// set from env
password = env.COUCH_DB_PASSWORD
} else if (!env.isTest()) { } else if (!env.isTest()) {
throw new Error("CouchDB password not set") throw new Error("CouchDB password not set")
} }

View File

@ -9,6 +9,20 @@ export async function directCouchCall(
) { ) {
let { url, cookie } = getCouchInfo() let { url, cookie } = getCouchInfo()
const couchUrl = `${url}/${path}` const couchUrl = `${url}/${path}`
return await directCouchUrlCall({ url: couchUrl, cookie, method, body })
}
export async function directCouchUrlCall({
url,
cookie,
method,
body,
}: {
url: string
cookie: string
method: string
body?: any
}) {
const params: any = { const params: any = {
method: method, method: method,
headers: { headers: {
@ -19,7 +33,7 @@ export async function directCouchCall(
params.body = JSON.stringify(body) params.body = JSON.stringify(body)
params.headers["Content-Type"] = "application/json" params.headers["Content-Type"] = "application/json"
} }
return await fetch(checkSlashesInUrl(encodeURI(couchUrl)), params) return await fetch(checkSlashesInUrl(encodeURI(url)), params)
} }
export async function directCouchQuery( export async function directCouchQuery(

View File

@ -434,7 +434,7 @@ export class QueryBuilder<T> {
}) })
} }
if (this.#query.empty) { if (this.#query.empty) {
build(this.#query.empty, (key: string) => `!${key}:["" TO *]`) build(this.#query.empty, (key: string) => `(*:* -${key}:["" TO *])`)
} }
if (this.#query.notEmpty) { if (this.#query.notEmpty) {
build(this.#query.notEmpty, (key: string) => `${key}:["" TO *]`) build(this.#query.notEmpty, (key: string) => `${key}:["" TO *]`)

View File

@ -69,10 +69,10 @@ function findVersion() {
try { try {
const packageJsonFile = findFileInAncestors("package.json", process.cwd()) const packageJsonFile = findFileInAncestors("package.json", process.cwd())
const content = readFileSync(packageJsonFile!, "utf-8") const content = readFileSync(packageJsonFile!, "utf-8")
const version = JSON.parse(content).version return JSON.parse(content).version
return version
} catch { } catch {
throw new Error("Cannot find a valid version in its package.json") // throwing an error here is confusing/causes backend-core to be hard to import
return undefined
} }
} }
@ -95,7 +95,8 @@ const environment = {
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET, GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
SALT_ROUNDS: process.env.SALT_ROUNDS, SALT_ROUNDS: process.env.SALT_ROUNDS,
REDIS_URL: process.env.REDIS_URL || "localhost:6379", REDIS_URL: process.env.REDIS_URL || "localhost:6379",
REDIS_PASSWORD: process.env.REDIS_PASSWORD || "budibase", REDIS_PASSWORD: process.env.REDIS_PASSWORD,
REDIS_CLUSTERED: process.env.REDIS_CLUSTERED,
MOCK_REDIS: process.env.MOCK_REDIS, MOCK_REDIS: process.env.MOCK_REDIS,
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY, MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY, MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
@ -154,6 +155,7 @@ const environment = {
? process.env.ENABLE_SSO_MAINTENANCE_MODE ? process.env.ENABLE_SSO_MAINTENANCE_MODE
: false, : false,
VERSION: findVersion(), VERSION: findVersion(),
DISABLE_PINO_LOGGER: process.env.DISABLE_PINO_LOGGER,
_set(key: any, value: any) { _set(key: any, value: any) {
process.env[key] = value process.env[key] = value
// @ts-ignore // @ts-ignore

View File

@ -3,7 +3,6 @@ import {
Event, Event,
LicenseActivatedEvent, LicenseActivatedEvent,
LicensePlanChangedEvent, LicensePlanChangedEvent,
LicenseTierChangedEvent,
PlanType, PlanType,
Account, Account,
LicensePortalOpenedEvent, LicensePortalOpenedEvent,
@ -11,22 +10,23 @@ import {
LicenseCheckoutOpenedEvent, LicenseCheckoutOpenedEvent,
LicensePaymentFailedEvent, LicensePaymentFailedEvent,
LicensePaymentRecoveredEvent, LicensePaymentRecoveredEvent,
PriceDuration,
} from "@budibase/types" } from "@budibase/types"
async function tierChanged(account: Account, from: number, to: number) { async function planChanged(
const properties: LicenseTierChangedEvent = { account: Account,
accountId: account.accountId, opts: {
to, from: PlanType
from, to: PlanType
fromQuantity: number | undefined
toQuantity: number | undefined
fromDuration: PriceDuration | undefined
toDuration: PriceDuration | undefined
} }
await publishEvent(Event.LICENSE_TIER_CHANGED, properties) ) {
}
async function planChanged(account: Account, from: PlanType, to: PlanType) {
const properties: LicensePlanChangedEvent = { const properties: LicensePlanChangedEvent = {
accountId: account.accountId, accountId: account.accountId,
to, ...opts,
from,
} }
await publishEvent(Event.LICENSE_PLAN_CHANGED, properties) await publishEvent(Event.LICENSE_PLAN_CHANGED, properties)
} }
@ -74,7 +74,6 @@ async function paymentRecovered(account: Account) {
} }
export default { export default {
tierChanged,
planChanged, planChanged,
activated, activated,
checkoutOpened, checkoutOpened,

View File

@ -1,5 +1,5 @@
export * as correlation from "./correlation/correlation" export * as correlation from "./correlation/correlation"
export { logger, disableLogger } from "./pino/logger" export { logger } from "./pino/logger"
export * from "./alerts" export * from "./alerts"
// turn off or on context logging i.e. tenantId, appId etc // turn off or on context logging i.e. tenantId, appId etc

View File

@ -5,19 +5,10 @@ import * as correlation from "../correlation"
import { IdentityType } from "@budibase/types" import { IdentityType } from "@budibase/types"
import { LOG_CONTEXT } from "../index" import { LOG_CONTEXT } from "../index"
// CORE LOGGERS - for disabling
const BUILT_INS = {
log: console.log,
error: console.error,
info: console.info,
warn: console.warn,
trace: console.trace,
debug: console.debug,
}
// LOGGER // LOGGER
let pinoInstance: pino.Logger | undefined
if (!env.DISABLE_PINO_LOGGER) {
const pinoOptions: LoggerOptions = { const pinoOptions: LoggerOptions = {
level: env.LOG_LEVEL, level: env.LOG_LEVEL,
formatters: { formatters: {
@ -40,16 +31,7 @@ if (env.isDev()) {
} }
} }
export const logger = pino(pinoOptions) pinoInstance = pino(pinoOptions)
export function disableLogger() {
console.log = BUILT_INS.log
console.error = BUILT_INS.error
console.info = BUILT_INS.info
console.warn = BUILT_INS.warn
console.trace = BUILT_INS.trace
console.debug = BUILT_INS.debug
}
// CONSOLE OVERRIDES // CONSOLE OVERRIDES
@ -57,6 +39,7 @@ interface MergingObject {
objects?: any[] objects?: any[]
tenantId?: string tenantId?: string
appId?: string appId?: string
automationId?: string
identityId?: string identityId?: string
identityType?: IdentityType identityType?: IdentityType
correlationId?: string correlationId?: string
@ -104,36 +87,62 @@ function getLogParams(args: any[]): [MergingObject, string] {
contextObject = { contextObject = {
tenantId: getTenantId(), tenantId: getTenantId(),
appId: getAppId(), appId: getAppId(),
automationId: getAutomationId(),
identityId: identity?._id, identityId: identity?._id,
identityType: identity?.type, identityType: identity?.type,
correlationId: correlation.getId(), correlationId: correlation.getId(),
} }
} }
const mergingObject = { const mergingObject: any = {
objects: objects.length ? objects : undefined,
err: error, err: error,
...contextObject, ...contextObject,
} }
if (objects.length) {
// init generic data object for params supplied that don't have a
// '_logKey' field. This prints an object using argument index as the key
// e.g. { 0: {}, 1: {} }
const data: any = {}
let dataIndex = 0
for (let i = 0; i < objects.length; i++) {
const object = objects[i]
// the object has specified a log key
// use this instead of generic key
const logKey = object._logKey
if (logKey) {
delete object._logKey
mergingObject[logKey] = object
} else {
data[dataIndex] = object
dataIndex++
}
}
if (Object.keys(data).length) {
mergingObject.data = data
}
}
return [mergingObject, message] return [mergingObject, message]
} }
console.log = (...arg: any[]) => { console.log = (...arg: any[]) => {
const [obj, msg] = getLogParams(arg) const [obj, msg] = getLogParams(arg)
logger.info(obj, msg) pinoInstance?.info(obj, msg)
} }
console.info = (...arg: any[]) => { console.info = (...arg: any[]) => {
const [obj, msg] = getLogParams(arg) const [obj, msg] = getLogParams(arg)
logger.info(obj, msg) pinoInstance?.info(obj, msg)
} }
console.warn = (...arg: any[]) => { console.warn = (...arg: any[]) => {
const [obj, msg] = getLogParams(arg) const [obj, msg] = getLogParams(arg)
logger.warn(obj, msg) pinoInstance?.warn(obj, msg)
} }
console.error = (...arg: any[]) => { console.error = (...arg: any[]) => {
const [obj, msg] = getLogParams(arg) const [obj, msg] = getLogParams(arg)
logger.error(obj, msg) pinoInstance?.error(obj, msg)
} }
/** /**
@ -147,12 +156,12 @@ console.trace = (...arg: any[]) => {
// to get stack trace // to get stack trace
obj.err = new Error() obj.err = new Error()
} }
logger.trace(obj, msg) pinoInstance?.trace(obj, msg)
} }
console.debug = (...arg: any) => { console.debug = (...arg: any) => {
const [obj, msg] = getLogParams(arg) const [obj, msg] = getLogParams(arg)
logger.debug(obj, msg) pinoInstance?.debug(obj, msg)
} }
// CONTEXT // CONTEXT
@ -177,6 +186,16 @@ const getAppId = () => {
return appId return appId
} }
const getAutomationId = () => {
let appId
try {
appId = context.getAutomationId()
} catch (e) {
// do nothing
}
return appId
}
const getIdentity = () => { const getIdentity = () => {
let identity let identity
try { try {
@ -186,3 +205,6 @@ const getIdentity = () => {
} }
return identity return identity
} }
}
export const logger = pinoInstance

View File

@ -128,6 +128,7 @@ class InMemoryQueue {
on() { on() {
// do nothing // do nothing
return this
} }
async waitForCompletion() { async waitForCompletion() {

View File

@ -1,5 +1,6 @@
import { Job, JobId, Queue } from "bull" import { Job, JobId, Queue } from "bull"
import { JobQueue } from "./constants" import { JobQueue } from "./constants"
import * as context from "../context"
export type StalledFn = (job: Job) => Promise<void> export type StalledFn = (job: Job) => Promise<void>
@ -31,71 +32,164 @@ function handleStalled(queue: Queue, removeStalledCb?: StalledFn) {
}) })
} }
function logging(queue: Queue, jobQueue: JobQueue) { function getLogParams(
let eventType: string eventType: QueueEventType,
switch (jobQueue) { event: BullEvent,
case JobQueue.AUTOMATION: opts: {
eventType = "automation-event" job?: Job
break jobId?: JobId
case JobQueue.APP_BACKUP: error?: Error
eventType = "app-backup-event" } = {},
break extra: any = {}
) {
const message = `[BULL] ${eventType}=${event}`
const err = opts.error
const bullLog = {
_logKey: "bull",
eventType,
event,
job: opts.job,
jobId: opts.jobId || opts.job?.id,
...extra,
} }
if (process.env.NODE_DEBUG?.includes("bull")) {
let automationLog
if (opts.job?.data?.automation) {
automationLog = {
_logKey: "automation",
trigger: opts.job
? opts.job.data.automation.definition.trigger.event
: undefined,
}
}
return [message, err, bullLog, automationLog]
}
enum BullEvent {
ERROR = "error",
WAITING = "waiting",
ACTIVE = "active",
STALLED = "stalled",
PROGRESS = "progress",
COMPLETED = "completed",
FAILED = "failed",
PAUSED = "paused",
RESUMED = "resumed",
CLEANED = "cleaned",
DRAINED = "drained",
REMOVED = "removed",
}
enum QueueEventType {
AUTOMATION_EVENT = "automation-event",
APP_BACKUP_EVENT = "app-backup-event",
AUDIT_LOG_EVENT = "audit-log-event",
SYSTEM_EVENT = "system-event",
}
const EventTypeMap: { [key in JobQueue]: QueueEventType } = {
[JobQueue.AUTOMATION]: QueueEventType.AUTOMATION_EVENT,
[JobQueue.APP_BACKUP]: QueueEventType.APP_BACKUP_EVENT,
[JobQueue.AUDIT_LOG]: QueueEventType.AUDIT_LOG_EVENT,
[JobQueue.SYSTEM_EVENT_QUEUE]: QueueEventType.SYSTEM_EVENT,
}
function logging(queue: Queue, jobQueue: JobQueue) {
const eventType = EventTypeMap[jobQueue]
function doInJobContext(job: Job, task: any) {
// if this is an automation job try to get the app id
const appId = job.data.event?.appId
if (appId) {
return context.doInContext(appId, task)
} else {
task()
}
}
queue queue
.on("error", (error: any) => { .on(BullEvent.STALLED, async (job: Job) => {
// An error occurred.
console.error(`${eventType}=error error=${JSON.stringify(error)}`)
})
.on("waiting", (jobId: JobId) => {
// A Job is waiting to be processed as soon as a worker is idling.
console.log(`${eventType}=waiting jobId=${jobId}`)
})
.on("active", (job: Job, jobPromise: any) => {
// A job has started. You can use `jobPromise.cancel()`` to abort it.
console.log(`${eventType}=active jobId=${job.id}`)
})
.on("stalled", (job: Job) => {
// A job has been marked as stalled. This is useful for debugging job // A job has been marked as stalled. This is useful for debugging job
// workers that crash or pause the event loop. // workers that crash or pause the event loop.
console.error( await doInJobContext(job, () => {
`${eventType}=stalled jobId=${job.id} job=${JSON.stringify(job)}` console.error(...getLogParams(eventType, BullEvent.STALLED, { job }))
})
})
.on(BullEvent.ERROR, (error: any) => {
// An error occurred.
console.error(...getLogParams(eventType, BullEvent.ERROR, { error }))
})
if (process.env.NODE_DEBUG?.includes("bull")) {
queue
.on(BullEvent.WAITING, (jobId: JobId) => {
// A Job is waiting to be processed as soon as a worker is idling.
console.info(...getLogParams(eventType, BullEvent.WAITING, { jobId }))
})
.on(BullEvent.ACTIVE, async (job: Job, jobPromise: any) => {
// A job has started. You can use `jobPromise.cancel()`` to abort it.
await doInJobContext(job, () => {
console.info(...getLogParams(eventType, BullEvent.ACTIVE, { job }))
})
})
.on(BullEvent.PROGRESS, async (job: Job, progress: any) => {
// A job's progress was updated
await doInJobContext(job, () => {
console.info(
...getLogParams(
eventType,
BullEvent.PROGRESS,
{ job },
{ progress }
)
) )
}) })
.on("progress", (job: Job, progress: any) => {
// A job's progress was updated!
console.log(
`${eventType}=progress jobId=${job.id} progress=${progress}`
)
}) })
.on("completed", (job: Job, result) => { .on(BullEvent.COMPLETED, async (job: Job, result) => {
// A job successfully completed with a `result`. // A job successfully completed with a `result`.
console.log(`${eventType}=completed jobId=${job.id} result=${result}`) await doInJobContext(job, () => {
console.info(
...getLogParams(eventType, BullEvent.COMPLETED, { job }, { result })
)
}) })
.on("failed", (job, err: any) => { })
.on(BullEvent.FAILED, async (job: Job, error: any) => {
// A job failed with reason `err`! // A job failed with reason `err`!
console.log(`${eventType}=failed jobId=${job.id} error=${err}`) await doInJobContext(job, () => {
console.error(
...getLogParams(eventType, BullEvent.FAILED, { job, error })
)
}) })
.on("paused", () => { })
.on(BullEvent.PAUSED, () => {
// The queue has been paused. // The queue has been paused.
console.log(`${eventType}=paused`) console.info(...getLogParams(eventType, BullEvent.PAUSED))
}) })
.on("resumed", (job: Job) => { .on(BullEvent.RESUMED, () => {
// The queue has been resumed. // The queue has been resumed.
console.log(`${eventType}=paused jobId=${job.id}`) console.info(...getLogParams(eventType, BullEvent.RESUMED))
}) })
.on("cleaned", (jobs: Job[], type: string) => { .on(BullEvent.CLEANED, (jobs: Job[], type: string) => {
// Old jobs have been cleaned from the queue. `jobs` is an array of cleaned // Old jobs have been cleaned from the queue. `jobs` is an array of cleaned
// jobs, and `type` is the type of jobs cleaned. // jobs, and `type` is the type of jobs cleaned.
console.log(`${eventType}=cleaned length=${jobs.length} type=${type}`) console.info(
...getLogParams(
eventType,
BullEvent.CLEANED,
{},
{ length: jobs.length, type }
)
)
}) })
.on("drained", () => { .on(BullEvent.DRAINED, () => {
// Emitted every time the queue has processed all the waiting jobs (even if there can be some delayed jobs not yet processed) // Emitted every time the queue has processed all the waiting jobs (even if there can be some delayed jobs not yet processed)
console.log(`${eventType}=drained`) console.info(...getLogParams(eventType, BullEvent.DRAINED))
}) })
.on("removed", (job: Job) => { .on(BullEvent.REMOVED, (job: Job) => {
// A job successfully removed. // A job successfully removed.
console.log(`${eventType}=removed jobId=${job.id}`) console.info(...getLogParams(eventType, BullEvent.REMOVED, { job }))
}) })
} }
} }

View File

@ -12,7 +12,7 @@ import * as timers from "../timers"
const RETRY_PERIOD_MS = 2000 const RETRY_PERIOD_MS = 2000
const STARTUP_TIMEOUT_MS = 5000 const STARTUP_TIMEOUT_MS = 5000
const CLUSTERED = false const CLUSTERED = env.REDIS_CLUSTERED
const DEFAULT_SELECT_DB = SelectableDatabase.DEFAULT const DEFAULT_SELECT_DB = SelectableDatabase.DEFAULT
// for testing just generate the client once // for testing just generate the client once
@ -81,7 +81,7 @@ function init(selectDb = DEFAULT_SELECT_DB) {
if (client) { if (client) {
client.disconnect() client.disconnect()
} }
const { redisProtocolUrl, opts, host, port } = getRedisOptions(CLUSTERED) const { redisProtocolUrl, opts, host, port } = getRedisOptions()
if (CLUSTERED) { if (CLUSTERED) {
client = new Redis.Cluster([{ host, port }], opts) client = new Redis.Cluster([{ host, port }], opts)

View File

@ -85,7 +85,7 @@ export const doWithLock = async <T>(
opts: LockOptions, opts: LockOptions,
task: () => Promise<T> task: () => Promise<T>
): Promise<RedlockExecution<T>> => { ): Promise<RedlockExecution<T>> => {
const redlock = await getClient(opts.type) const redlock = await getClient(opts.type, opts.customOptions)
let lock let lock
try { try {
// determine lock name // determine lock name

View File

@ -57,7 +57,7 @@ export enum SelectableDatabase {
UNUSED_14 = 15, UNUSED_14 = 15,
} }
export function getRedisOptions(clustered = false) { export function getRedisOptions() {
let password = env.REDIS_PASSWORD let password = env.REDIS_PASSWORD
let url: string[] | string = env.REDIS_URL.split("//") let url: string[] | string = env.REDIS_URL.split("//")
// get rid of the protocol // get rid of the protocol
@ -83,7 +83,7 @@ export function getRedisOptions(clustered = false) {
const opts: any = { const opts: any = {
connectTimeout: CONNECT_TIMEOUT_MS, connectTimeout: CONNECT_TIMEOUT_MS,
} }
if (clustered) { if (env.REDIS_CLUSTERED) {
opts.redisOptions = {} opts.redisOptions = {}
opts.redisOptions.tls = {} opts.redisOptions.tls = {}
opts.redisOptions.password = password opts.redisOptions.password = password

View File

@ -5,6 +5,7 @@ import * as db from "../../db"
import { Header } from "../../constants" import { Header } from "../../constants"
import { newid } from "../../utils" import { newid } from "../../utils"
import env from "../../environment" import env from "../../environment"
import { BBContext } from "@budibase/types"
describe("utils", () => { describe("utils", () => {
const config = new DBTestConfiguration() const config = new DBTestConfiguration()
@ -106,4 +107,85 @@ describe("utils", () => {
expect(actual).toBe(undefined) expect(actual).toBe(undefined)
}) })
}) })
describe("isServingBuilder", () => {
let ctx: BBContext
const expectResult = (result: boolean) =>
expect(utils.isServingBuilder(ctx)).toBe(result)
beforeEach(() => {
ctx = structures.koa.newContext()
})
it("returns true if current path is in builder", async () => {
ctx.path = "/builder/app/app_"
expectResult(true)
})
it("returns false if current path doesn't have '/' suffix", async () => {
ctx.path = "/builder/app"
expectResult(false)
ctx.path = "/xx"
expectResult(false)
})
})
describe("isServingBuilderPreview", () => {
let ctx: BBContext
const expectResult = (result: boolean) =>
expect(utils.isServingBuilderPreview(ctx)).toBe(result)
beforeEach(() => {
ctx = structures.koa.newContext()
})
it("returns true if current path is in builder preview", async () => {
ctx.path = "/app/preview/xx"
expectResult(true)
})
it("returns false if current path is not in builder preview", async () => {
ctx.path = "/builder"
expectResult(false)
ctx.path = "/xx"
expectResult(false)
})
})
describe("isPublicAPIRequest", () => {
let ctx: BBContext
const expectResult = (result: boolean) =>
expect(utils.isPublicApiRequest(ctx)).toBe(result)
beforeEach(() => {
ctx = structures.koa.newContext()
})
it("returns true if current path remains to public API", async () => {
ctx.path = "/api/public/v1/invoices"
expectResult(true)
ctx.path = "/api/public/v1"
expectResult(true)
ctx.path = "/api/public/v2"
expectResult(true)
ctx.path = "/api/public/v21"
expectResult(true)
})
it("returns false if current path doesn't remain to public API", async () => {
ctx.path = "/api/public"
expectResult(false)
ctx.path = "/xx"
expectResult(false)
})
})
}) })

View File

@ -1,11 +1,5 @@
import { getAllApps, queryGlobalView } from "../db" import { getAllApps } from "../db"
import { import { Header, MAX_VALID_DATE, DocumentType, SEPARATOR } from "../constants"
Header,
MAX_VALID_DATE,
DocumentType,
SEPARATOR,
ViewName,
} from "../constants"
import env from "../environment" import env from "../environment"
import * as tenancy from "../tenancy" import * as tenancy from "../tenancy"
import * as context from "../context" import * as context from "../context"
@ -23,7 +17,9 @@ const APP_PREFIX = DocumentType.APP + SEPARATOR
const PROD_APP_PREFIX = "/app/" const PROD_APP_PREFIX = "/app/"
const BUILDER_PREVIEW_PATH = "/app/preview" const BUILDER_PREVIEW_PATH = "/app/preview"
const BUILDER_REFERER_PREFIX = "/builder/app/" const BUILDER_PREFIX = "/builder"
const BUILDER_APP_PREFIX = `${BUILDER_PREFIX}/app/`
const PUBLIC_API_PREFIX = "/api/public/v"
function confirmAppId(possibleAppId: string | undefined) { function confirmAppId(possibleAppId: string | undefined) {
return possibleAppId && possibleAppId.startsWith(APP_PREFIX) return possibleAppId && possibleAppId.startsWith(APP_PREFIX)
@ -69,6 +65,18 @@ export function isServingApp(ctx: Ctx) {
return false return false
} }
export function isServingBuilder(ctx: Ctx): boolean {
return ctx.path.startsWith(BUILDER_APP_PREFIX)
}
export function isServingBuilderPreview(ctx: Ctx): boolean {
return ctx.path.startsWith(BUILDER_PREVIEW_PATH)
}
export function isPublicApiRequest(ctx: Ctx): boolean {
return ctx.path.startsWith(PUBLIC_API_PREFIX)
}
/** /**
* Given a request tries to find the appId, which can be located in various places * Given a request tries to find the appId, which can be located in various places
* @param {object} ctx The main request body to look through. * @param {object} ctx The main request body to look through.
@ -110,7 +118,7 @@ export async function getAppIdFromCtx(ctx: Ctx) {
// make sure this is performed after prod app url resolution, in case the // make sure this is performed after prod app url resolution, in case the
// referer header is present from a builder redirect // referer header is present from a builder redirect
const referer = ctx.request.headers.referer const referer = ctx.request.headers.referer
if (!appId && referer?.includes(BUILDER_REFERER_PREFIX)) { if (!appId && referer?.includes(BUILDER_APP_PREFIX)) {
const refererId = parseAppIdFromUrl(ctx.request.headers.referer) const refererId = parseAppIdFromUrl(ctx.request.headers.referer)
appId = confirmAppId(refererId) appId = confirmAppId(refererId)
} }

View File

@ -123,7 +123,6 @@ beforeAll(async () => {
jest.spyOn(events.plugin, "imported") jest.spyOn(events.plugin, "imported")
jest.spyOn(events.plugin, "deleted") jest.spyOn(events.plugin, "deleted")
jest.spyOn(events.license, "tierChanged")
jest.spyOn(events.license, "planChanged") jest.spyOn(events.license, "planChanged")
jest.spyOn(events.license, "activated") jest.spyOn(events.license, "activated")
jest.spyOn(events.license, "checkoutOpened") jest.spyOn(events.license, "checkoutOpened")

View File

@ -7,16 +7,29 @@ import {
PlanType, PlanType,
PriceDuration, PriceDuration,
PurchasedPlan, PurchasedPlan,
PurchasedPrice,
Quotas, Quotas,
Subscription, Subscription,
} from "@budibase/types" } from "@budibase/types"
export function price(): PurchasedPrice {
return {
amount: 10000,
amountMonthly: 10000,
currency: "usd",
duration: PriceDuration.MONTHLY,
priceId: "price_123",
dayPasses: undefined,
isPerUser: true,
}
}
export const plan = (type: PlanType = PlanType.FREE): PurchasedPlan => { export const plan = (type: PlanType = PlanType.FREE): PurchasedPlan => {
return { return {
type, type,
usesInvoicing: false, usesInvoicing: false,
minUsers: 1,
model: PlanModel.PER_USER, model: PlanModel.PER_USER,
price: type !== PlanType.FREE ? price() : undefined,
} }
} }

View File

@ -7,11 +7,6 @@
"@budibase/types": ["../types/src"] "@budibase/types": ["../types/src"]
} }
}, },
"references": [
{ "path": "../types" } "exclude": ["node_modules", "dist"]
],
"exclude": [
"node_modules",
"dist",
]
} }

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/bbui", "name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.", "description": "A UI solution used in the different Budibase projects.",
"version": "2.5.6-alpha.30", "version": "0.0.1",
"license": "MPL-2.0", "license": "MPL-2.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"module": "dist/bbui.es.js", "module": "dist/bbui.es.js",
@ -38,8 +38,8 @@
], ],
"dependencies": { "dependencies": {
"@adobe/spectrum-css-workflow-icons": "1.2.1", "@adobe/spectrum-css-workflow-icons": "1.2.1",
"@budibase/shared-core": "2.5.6-alpha.30", "@budibase/shared-core": "0.0.1",
"@budibase/string-templates": "2.5.6-alpha.30", "@budibase/string-templates": "0.0.1",
"@spectrum-css/accordion": "3.0.24", "@spectrum-css/accordion": "3.0.24",
"@spectrum-css/actionbutton": "1.0.1", "@spectrum-css/actionbutton": "1.0.1",
"@spectrum-css/actiongroup": "1.0.1", "@spectrum-css/actiongroup": "1.0.1",
@ -84,7 +84,7 @@
"@spectrum-css/vars": "3.0.1", "@spectrum-css/vars": "3.0.1",
"dayjs": "^1.10.4", "dayjs": "^1.10.4",
"easymde": "^2.16.1", "easymde": "^2.16.1",
"svelte-flatpickr": "^3.3.2", "svelte-flatpickr": "3.2.3",
"svelte-portal": "^1.0.0" "svelte-portal": "^1.0.0"
}, },
"resolutions": { "resolutions": {

View File

@ -1,4 +1,8 @@
const ignoredClasses = [".flatpickr-calendar", ".spectrum-Popover"] const ignoredClasses = [
".flatpickr-calendar",
".spectrum-Popover",
".download-js-link",
]
let clickHandlers = [] let clickHandlers = []
/** /**
@ -22,8 +26,8 @@ const handleClick = event => {
} }
// Ignore clicks for modals, unless the handler is registered from a modal // Ignore clicks for modals, unless the handler is registered from a modal
const sourceInModal = handler.anchor.closest(".spectrum-Modal") != null const sourceInModal = handler.anchor.closest(".spectrum-Underlay") != null
const clickInModal = event.target.closest(".spectrum-Modal") != null const clickInModal = event.target.closest(".spectrum-Underlay") != null
if (clickInModal && !sourceInModal) { if (clickInModal && !sourceInModal) {
return return
} }

View File

@ -2,6 +2,7 @@
import "@spectrum-css/button/dist/index-vars.css" import "@spectrum-css/button/dist/index-vars.css"
import Tooltip from "../Tooltip/Tooltip.svelte" import Tooltip from "../Tooltip/Tooltip.svelte"
export let type
export let disabled = false export let disabled = false
export let size = "M" export let size = "M"
export let cta = false export let cta = false
@ -21,6 +22,7 @@
<button <button
{id} {id}
{type}
class:spectrum-Button--cta={cta} class:spectrum-Button--cta={cta}
class:spectrum-Button--primary={primary} class:spectrum-Button--primary={primary}
class:spectrum-Button--secondary={secondary} class:spectrum-Button--secondary={secondary}
@ -73,6 +75,7 @@
button { button {
position: relative; position: relative;
} }
.spectrum-Button-label { .spectrum-Button-label {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;

View File

@ -0,0 +1,19 @@
<script>
import { slide } from "svelte/transition"
export let error = null
</script>
<div transition:slide|local={{ duration: 130 }} class="error-message">
{error}
</div>
<style>
.error-message {
background: var(--spectrum-global-color-red-400);
color: white;
font-size: 14px;
padding: 6px 16px;
font-weight: 500;
}
</style>

View File

@ -1,7 +1,7 @@
<script> <script>
import Icon from "../Icon/Icon.svelte" import Icon from "../Icon/Icon.svelte"
import { getContext, onMount } from "svelte" import { getContext, onMount } from "svelte"
import { slide } from "svelte/transition" import ErrorMessage from "./ErrorMessage.svelte"
export let disabled = false export let disabled = false
export let error = null export let error = null
@ -55,9 +55,7 @@
{/if} {/if}
</div> </div>
{#if error} {#if error}
<div transition:slide|local={{ duration: 130 }} class="error-message"> <ErrorMessage {error} />
{error}
</div>
{/if} {/if}
</div> </div>
@ -110,13 +108,6 @@
.field { .field {
flex: 1 1 auto; flex: 1 1 auto;
} }
.error-message {
background: var(--spectrum-global-color-red-400);
color: white;
font-size: 14px;
padding: 6px 16px;
font-weight: 500;
}
.error-icon { .error-icon {
flex: 0 0 auto; flex: 0 0 auto;
} }

View File

@ -4,3 +4,4 @@ export { default as FancySelect } from "./FancySelect.svelte"
export { default as FancyButton } from "./FancyButton.svelte" export { default as FancyButton } from "./FancyButton.svelte"
export { default as FancyForm } from "./FancyForm.svelte" export { default as FancyForm } from "./FancyForm.svelte"
export { default as FancyButtonRadio } from "./FancyButtonRadio.svelte" export { default as FancyButtonRadio } from "./FancyButtonRadio.svelte"
export { default as ErrorMessage } from "./ErrorMessage.svelte"

View File

@ -18,10 +18,14 @@
export let ignoreTimezones = false export let ignoreTimezones = false
export let time24hr = false export let time24hr = false
export let range = false export let range = false
export let flatpickr
export let useKeyboardShortcuts = true
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const flatpickrId = `${uuid()}-wrapper` const flatpickrId = `${uuid()}-wrapper`
let open = false let open = false
let flatpickr, flatpickrOptions let flatpickrOptions
// Another classic flatpickr issue. Errors were randomly being thrown due to // Another classic flatpickr issue. Errors were randomly being thrown due to
// flatpickr internal code. Making sure that "destroy" is a valid function // flatpickr internal code. Making sure that "destroy" is a valid function
@ -59,6 +63,8 @@
dispatch("change", timestamp.toISOString()) dispatch("change", timestamp.toISOString())
} }
}, },
onOpen: () => dispatch("open"),
onClose: () => dispatch("close"),
} }
$: redrawOptions = { $: redrawOptions = {
@ -113,12 +119,16 @@
const onOpen = () => { const onOpen = () => {
open = true open = true
if (useKeyboardShortcuts) {
document.addEventListener("keyup", clearDateOnBackspace) document.addEventListener("keyup", clearDateOnBackspace)
} }
}
const onClose = () => { const onClose = () => {
open = false open = false
if (useKeyboardShortcuts) {
document.removeEventListener("keyup", clearDateOnBackspace) document.removeEventListener("keyup", clearDateOnBackspace)
}
// Manually blur all input fields since flatpickr creates a second // Manually blur all input fields since flatpickr creates a second
// duplicate input field. // duplicate input field.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"> <g id="Make-App-Icon-Circle" transform="translate(3757 -1767)"> <circle id="Ellipse_10" data-name="Ellipse 10" cx="256" cy="256" r="256" transform="translate(-3757 1767)" fill="#6d00cc"/> <path id="Path_141560" data-name="Path 141560" d="M244.78,14.544a7.187,7.187,0,0,0-7.186,7.192V213.927a7.19,7.19,0,0,0,7.186,7.192h52.063a7.187,7.187,0,0,0,7.186-7.192V21.736a7.183,7.183,0,0,0-7.186-7.192ZM92.066,17.083,5.77,188.795a7.191,7.191,0,0,0,3.192,9.654l46.514,23.379a7.184,7.184,0,0,0,9.654-3.2l86.3-171.711a7.184,7.184,0,0,0-3.2-9.654L101.719,13.886a7.2,7.2,0,0,0-9.654,3.2m72.592.614L127.731,204.876a7.189,7.189,0,0,0,5.632,8.442l51.028,10.306a7.2,7.2,0,0,0,8.481-5.665L229.8,30.786a7.19,7.19,0,0,0-5.637-8.442L173.133,12.038a7.391,7.391,0,0,0-1.427-.144,7.194,7.194,0,0,0-7.048,5.8" transform="translate(-3676.356 1905.425)" fill="#fff"/> </g> </svg>

After

Width:  |  Height:  |  Size: 951 B

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/builder", "name": "@budibase/builder",
"version": "2.5.6-alpha.30", "version": "0.0.1",
"license": "GPL-3.0", "license": "GPL-3.0",
"private": true, "private": true,
"scripts": { "scripts": {
@ -58,11 +58,11 @@
} }
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "2.5.6-alpha.30", "@budibase/bbui": "0.0.1",
"@budibase/client": "2.5.6-alpha.30", "@budibase/frontend-core": "0.0.1",
"@budibase/frontend-core": "2.5.6-alpha.30", "@budibase/shared-core": "0.0.1",
"@budibase/shared-core": "2.5.6-alpha.30", "@budibase/string-templates": "0.0.1",
"@budibase/string-templates": "2.5.6-alpha.30", "@budibase/types": "0.0.1",
"@fortawesome/fontawesome-svg-core": "^6.2.1", "@fortawesome/fontawesome-svg-core": "^6.2.1",
"@fortawesome/free-brands-svg-icons": "^6.2.1", "@fortawesome/free-brands-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1", "@fortawesome/free-solid-svg-icons": "^6.2.1",

View File

@ -147,6 +147,9 @@ const automationActions = store => ({
testData, testData,
}) })
if (!result?.trigger && !result?.steps?.length) { if (!result?.trigger && !result?.steps?.length) {
if (result?.err?.code === "usage_limit_exceeded") {
throw "You have exceeded your automation quota"
}
throw "Something went wrong testing your automation" throw "Something went wrong testing your automation"
} }
store.update(state => { store.update(state => {

View File

@ -134,6 +134,7 @@ export const getFrontendStore = () => {
previousTopNavPath: {}, previousTopNavPath: {},
version: application.version, version: application.version,
revertableVersion: application.revertableVersion, revertableVersion: application.revertableVersion,
upgradableVersion: application.upgradableVersion,
navigation: application.navigation || {}, navigation: application.navigation || {},
usedPlugins: application.usedPlugins || [], usedPlugins: application.usedPlugins || [],
})) }))

View File

@ -94,7 +94,7 @@
/> />
<span class="icon-spacing"> <span class="icon-spacing">
<Body size="XS"> <Body size="XS">
{idx.charAt(0).toUpperCase() + idx.slice(1)} {action.stepTitle || idx.charAt(0).toUpperCase() + idx.slice(1)}
</Body> </Body>
</span> </span>
</div> </div>

View File

@ -1,11 +1,11 @@
import DiscordLogo from "assets/discord.svg" import DiscordLogo from "assets/discord.svg"
import ZapierLogo from "assets/zapier.png" import ZapierLogo from "assets/zapier.png"
import IntegromatLogo from "assets/integromat.png" import MakeLogo from "assets/make.svg"
import SlackLogo from "assets/slack.svg" import SlackLogo from "assets/slack.svg"
export const externalActions = { export const externalActions = {
zapier: { name: "zapier", icon: ZapierLogo }, zapier: { name: "zapier", icon: ZapierLogo },
discord: { name: "discord", icon: DiscordLogo }, discord: { name: "discord", icon: DiscordLogo },
slack: { name: "slack", icon: SlackLogo }, slack: { name: "slack", icon: SlackLogo },
integromat: { name: "integromat", icon: IntegromatLogo }, integromat: { name: "integromat", icon: MakeLogo },
} }

View File

@ -52,7 +52,7 @@
await automationStore.actions.test($selectedAutomation, testData) await automationStore.actions.test($selectedAutomation, testData)
$automationStore.showTestPanel = true $automationStore.showTestPanel = true
} catch (error) { } catch (error) {
notifications.error("Error testing automation") notifications.error(error)
} }
} }
</script> </script>

View File

@ -61,11 +61,63 @@
$: isTrigger = block?.type === "TRIGGER" $: isTrigger = block?.type === "TRIGGER"
$: isUpdateRow = stepId === ActionStepID.UPDATE_ROW $: isUpdateRow = stepId === ActionStepID.UPDATE_ROW
/**
* TODO - Remove after November 2023
* *******************************
* Code added to provide backwards compatibility between Values 1,2,3,4,5
* and the new JSON body.
*/
let deprecatedSchemaProperties
$: {
if (block?.stepId === "integromat" || block?.stepId === "zapier") {
deprecatedSchemaProperties = schemaProperties.filter(
prop => !prop[0].startsWith("value")
)
if (!deprecatedSchemaProperties.map(entry => entry[0]).includes("body")) {
deprecatedSchemaProperties.push([
"body",
{
title: "Payload",
type: "json",
},
])
}
} else {
deprecatedSchemaProperties = schemaProperties
}
}
/****************************************************/
const getInputData = (testData, blockInputs) => { const getInputData = (testData, blockInputs) => {
let newInputData = testData || blockInputs let newInputData = testData || blockInputs
if (block.event === "app:trigger" && !newInputData?.fields) { if (block.event === "app:trigger" && !newInputData?.fields) {
newInputData = cloneDeep(blockInputs) newInputData = cloneDeep(blockInputs)
} }
/**
* TODO - Remove after November 2023
* *******************************
* Code added to provide backwards compatibility between Values 1,2,3,4,5
* and the new JSON body.
*/
if (
(block?.stepId === "integromat" || block?.stepId === "zapier") &&
!newInputData?.body?.value
) {
let deprecatedValues = {
...newInputData,
}
delete deprecatedValues.url
delete deprecatedValues.body
newInputData = {
url: newInputData.url,
body: {
value: JSON.stringify(deprecatedValues),
},
}
}
/**********************************/
inputData = newInputData inputData = newInputData
setDefaultEnumValues() setDefaultEnumValues()
} }
@ -239,7 +291,7 @@
</script> </script>
<div class="fields"> <div class="fields">
{#each schemaProperties as [key, value]} {#each deprecatedSchemaProperties as [key, value]}
<div class="block-field"> <div class="block-field">
{#if key !== "fields"} {#if key !== "fields"}
<Label <Label
@ -256,6 +308,28 @@
options={value.enum} options={value.enum}
getOptionLabel={(x, idx) => (value.pretty ? value.pretty[idx] : x)} 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"} {:else if value.customType === "column"}
<Select <Select
on:change={e => onChange(e, key)} on:change={e => onChange(e, key)}

View File

@ -22,6 +22,8 @@
export let rowCount export let rowCount
export let disableSorting = false export let disableSorting = false
export let customPlaceholder = false export let customPlaceholder = false
export let allowClickRows
export let allowEditing = true
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -109,7 +111,9 @@
{rowCount} {rowCount}
{disableSorting} {disableSorting}
{customPlaceholder} {customPlaceholder}
allowEditRows={allowEditing}
showAutoColumns={!hideAutocolumns} showAutoColumns={!hideAutocolumns}
{allowClickRows}
on:clickrelationship={e => selectRelationship(e.detail)} on:clickrelationship={e => selectRelationship(e.detail)}
on:sort on:sort
> >

View File

@ -58,6 +58,7 @@
{loading} {loading}
{type} {type}
rowCount={10} rowCount={10}
allowEditing={false}
bind:hideAutocolumns bind:hideAutocolumns
> >
<ViewFilterButton {view} /> <ViewFilterButton {view} />

View File

@ -39,7 +39,7 @@
{#if datasource} {#if datasource}
<div> <div>
<ActionButton icon="DataCorrelated" primary quiet on:click={modal.show}> <ActionButton icon="DataCorrelated" primary quiet on:click={modal.show}>
Define existing relationship Define relationship
</ActionButton> </ActionButton>
</div> </div>
<Modal bind:this={modal}> <Modal bind:this={modal}>

View File

@ -9,6 +9,7 @@
$: selectedRowArray = Object.keys($selectedRows).map(id => ({ _id: id })) $: selectedRowArray = Object.keys($selectedRows).map(id => ({ _id: id }))
</script> </script>
<span data-ignore-click-outside="true">
<ExportButton <ExportButton
{disabled} {disabled}
view={$tableId} view={$tableId}
@ -19,3 +20,10 @@
}} }}
selectedRows={selectedRowArray} selectedRows={selectedRowArray}
/> />
</span>
<style>
span {
display: contents;
}
</style>

View File

@ -2,19 +2,19 @@
import TableFilterButton from "../TableFilterButton.svelte" import TableFilterButton from "../TableFilterButton.svelte"
import { getContext } from "svelte" import { getContext } from "svelte"
const { columns, config, filter, table } = getContext("grid") const { columns, tableId, filter, table } = getContext("grid")
const onFilter = e => { const onFilter = e => {
filter.set(e.detail || []) filter.set(e.detail || [])
} }
</script> </script>
{#key $config.tableId} {#key $tableId}
<TableFilterButton <TableFilterButton
schema={$table?.schema} schema={$table?.schema}
filters={$filter} filters={$filter}
on:change={onFilter} on:change={onFilter}
disabled={!$columns.length} disabled={!$columns.length}
tableId={$config.tableId} tableId={$tableId}
/> />
{/key} {/key}

View File

@ -2,7 +2,7 @@
import ManageAccessButton from "../ManageAccessButton.svelte" import ManageAccessButton from "../ManageAccessButton.svelte"
import { getContext } from "svelte" import { getContext } from "svelte"
const { config } = getContext("grid") const { tableId } = getContext("grid")
</script> </script>
<ManageAccessButton resourceId={$config.tableId} /> <ManageAccessButton resourceId={$tableId} />

View File

@ -6,7 +6,8 @@
import NavItem from "components/common/NavItem.svelte" import NavItem from "components/common/NavItem.svelte"
import { goto, isActive } from "@roxi/routify" import { goto, isActive } from "@roxi/routify"
const alphabetical = (a, b) => a.name?.toLowerCase() > b.name?.toLowerCase() const alphabetical = (a, b) =>
a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1
export let sourceId export let sourceId

View File

@ -42,16 +42,7 @@ export const parseFile = e => {
reader.addEventListener("load", function (e) { reader.addEventListener("load", function (e) {
const fileData = e.target.result const fileData = e.target.result
if (file.type?.includes("json")) {
if (file.type === "text/csv") {
API.csvToJson(fileData)
.then(rows => {
resolveRows(rows)
})
.catch(() => {
reject("can't convert csv to json")
})
} else if (file.type === "application/json") {
const parsedFileData = JSON.parse(fileData) const parsedFileData = JSON.parse(fileData)
if (Array.isArray(parsedFileData)) { if (Array.isArray(parsedFileData)) {
@ -62,7 +53,13 @@ export const parseFile = e => {
reject("invalid json format") reject("invalid json format")
} }
} else { } else {
reject("invalid file type") API.csvToJson(fileData)
.then(rows => {
resolveRows(rows)
})
.catch(() => {
reject("cannot parse csv")
})
} }
}) })

View File

@ -17,6 +17,7 @@
export let highlighted = false export let highlighted = false
export let rightAlignIcon = false export let rightAlignIcon = false
export let id export let id
export let showTooltip = false
const scrollApi = getContext("scroll") const scrollApi = getContext("scroll")
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -84,7 +85,7 @@
<Icon color={iconColor} size="S" name={icon} /> <Icon color={iconColor} size="S" name={icon} />
</div> </div>
{/if} {/if}
<div class="text">{text}</div> <div class="text" title={showTooltip ? text : null}>{text}</div>
{#if withActions} {#if withActions}
<div class="actions"> <div class="actions">
<slot /> <slot />

View File

@ -9,7 +9,6 @@
} from "@budibase/bbui" } from "@budibase/bbui"
import { store } from "builderStore" import { store } from "builderStore"
import { API } from "api" import { API } from "api"
import clientPackage from "@budibase/client/package.json"
export function show() { export function show() {
updateModal.show() updateModal.show()
@ -25,9 +24,9 @@
$: appId = $store.appId $: appId = $store.appId
$: updateAvailable = $: updateAvailable =
clientPackage.version && $store.upgradableVersion &&
$store.version && $store.version &&
clientPackage.version !== $store.version $store.upgradableVersion !== $store.version
$: revertAvailable = $store.revertableVersion != null $: revertAvailable = $store.revertableVersion != null
const refreshAppPackage = async () => { const refreshAppPackage = async () => {
@ -46,7 +45,7 @@
// Don't wait for the async refresh, since this causes modal flashing // Don't wait for the async refresh, since this causes modal flashing
refreshAppPackage() refreshAppPackage()
notifications.success( notifications.success(
`App updated successfully to version ${clientPackage.version}` `App updated successfully to version ${$store.upgradableVersion}`
) )
} catch (err) { } catch (err) {
notifications.error(`Error updating app: ${err}`) notifications.error(`Error updating app: ${err}`)
@ -91,7 +90,7 @@
{#if updateAvailable} {#if updateAvailable}
<Body size="S"> <Body size="S">
This app is currently using version <b>{$store.version}</b>, but version This app is currently using version <b>{$store.version}</b>, but version
<b>{clientPackage.version}</b> is available. Updates can contain new features, <b>{$store.upgradableVersion}</b> is available. Updates can contain new features,
performance improvements and bug fixes. performance improvements and bug fixes.
</Body> </Body>
{:else} {:else}

View File

@ -18,6 +18,7 @@
export let tab = true export let tab = true
export let mode export let mode
export let editorHeight = 500 export let editorHeight = 500
export let editorWidth = 640
// export let parameters = [] // export let parameters = []
let width let width
@ -169,7 +170,9 @@
{#if label} {#if label}
<Label small>{label}</Label> <Label small>{label}</Label>
{/if} {/if}
<div style={`--code-mirror-height: ${editorHeight}px`}> <div
style={`--code-mirror-height: ${editorHeight}px; --code-mirror-width: ${editorWidth}px;`}
>
<textarea tabindex="0" bind:this={refs.editor} readonly {value} /> <textarea tabindex="0" bind:this={refs.editor} readonly {value} />
</div> </div>
@ -183,6 +186,7 @@
} }
div :global(.CodeMirror) { div :global(.CodeMirror) {
width: var(--code-mirror-width) !important;
height: var(--code-mirror-height) !important; height: var(--code-mirror-height) !important;
border-radius: var(--border-radius-s); border-radius: var(--border-radius-s);
font-family: var(--font-mono); font-family: var(--font-mono);

View File

@ -0,0 +1,31 @@
<script>
import { Modal, ModalContent, Body } from "@budibase/bbui"
let modal
export let onConfirm
export function show() {
modal.show()
}
export function hide() {
modal.hide()
}
</script>
<Modal bind:this={modal} on:hide={modal}>
<ModalContent
title="Your account is currently de-activated"
size="S"
showCancelButton={true}
showCloseIcon={false}
confirmText={"View plans"}
{onConfirm}
>
<Body size="S"
>Due to the free plan user limit being exceeded, your account has been
de-activated. Upgrade your plan to re-activate your account.</Body
>
</ModalContent>
</Modal>

View File

@ -3,7 +3,6 @@ import { temporalStore } from "builderStore"
import { admin, auth, licensing } from "stores/portal" import { admin, auth, licensing } from "stores/portal"
import { get } from "svelte/store" import { get } from "svelte/store"
import { BANNER_TYPES } from "@budibase/bbui" import { BANNER_TYPES } from "@budibase/bbui"
import { capitalise } from "helpers"
const oneDayInSeconds = 86400 const oneDayInSeconds = 86400
@ -146,20 +145,19 @@ const buildUsersAboveLimitBanner = EXPIRY_KEY => {
const userLicensing = get(licensing) const userLicensing = get(licensing)
return { return {
key: EXPIRY_KEY, key: EXPIRY_KEY,
type: BANNER_TYPES.WARNING, type: BANNER_TYPES.NEGATIVE,
criteria: () => { onChange: () => {
return userLicensing.warnUserLimit defaultCacheFn(EXPIRY_KEY)
}, },
message: `${capitalise( criteria: () => {
userLicensing.license.plan.type return userLicensing.errUserLimit
)} plan changes - Users will be limited to ${ },
userLicensing.userLimit message: "Your Budibase account is de-activated. Upgrade your plan",
} users in ${userLicensing.userLimitDays}`,
...{ ...{
extraButtonText: "Find out more", extraButtonText: "View plans",
extraButtonAction: () => { extraButtonAction: () => {
defaultCacheFn(ExpiringKeys.LICENSING_USERS_ABOVE_LIMIT_BANNER) defaultCacheFn(ExpiringKeys.LICENSING_USERS_ABOVE_LIMIT_BANNER)
window.location.href = "/builder/portal/users/users" window.location.href = "https://budibase.com/pricing/"
}, },
}, },
showCloseButton: true, showCloseButton: true,

View File

@ -6,6 +6,8 @@
export let app export let app
export let lockedAction
const handleDefaultClick = () => { const handleDefaultClick = () => {
if (window.innerWidth < 640) { if (window.innerWidth < 640) {
goToOverview() goToOverview()
@ -29,7 +31,7 @@
} }
</script> </script>
<div class="app-row" on:click={handleDefaultClick}> <div class="app-row" on:click={lockedAction || handleDefaultClick}>
<div class="title"> <div class="title">
<div class="app-icon"> <div class="app-icon">
<Icon size="L" name={app.icon?.name || "Apps"} color={app.icon?.color} /> <Icon size="L" name={app.icon?.name || "Apps"} color={app.icon?.color} />
@ -58,8 +60,11 @@
<div class="app-row-actions"> <div class="app-row-actions">
<AppLockModal {app} buttonSize="M" /> <AppLockModal {app} buttonSize="M" />
<Button size="S" secondary on:click={goToOverview}>Manage</Button> <Button size="S" secondary on:click={lockedAction || goToOverview}
<Button size="S" primary on:click={goToBuilder}>Edit</Button> >Manage</Button
>
<Button size="S" primary on:click={lockedAction || goToBuilder}>Edit</Button
>
</div> </div>
</div> </div>

View File

@ -27,7 +27,7 @@
onMount(() => { onMount(() => {
unlimited = isUnlimited() unlimited = isUnlimited()
percentage = getPercentage() percentage = getPercentage()
if (warnWhenFull && percentage === 100) { if (warnWhenFull && percentage >= 100) {
showWarning = true showWarning = true
} }
}) })

View File

@ -28,13 +28,16 @@
let inviting = false let inviting = false
let searchFocus = false let searchFocus = false
// Initially filter entities without app access
// Show all when false
let filterByAppAccess = true
let appInvites = [] let appInvites = []
let filteredInvites = [] let filteredInvites = []
let filteredUsers = [] let filteredUsers = []
let filteredGroups = [] let filteredGroups = []
let selectedGroup let selectedGroup
let userOnboardResponse = null let userOnboardResponse = null
let userLimitReachedModal let userLimitReachedModal
$: queryIsEmail = emailValidator(query) === true $: queryIsEmail = emailValidator(query) === true
@ -52,15 +55,32 @@
} }
const filterInvites = async query => { const filterInvites = async query => {
appInvites = await getInvites() if (!prodAppId) {
if (!query || query == "") {
filteredInvites = appInvites
return return
} }
filteredInvites = appInvites.filter(invite => invite.email.includes(query))
appInvites = await getInvites()
//On Focus behaviour
if (!filterByAppAccess && !query) {
filteredInvites =
appInvites.length > 100 ? appInvites.slice(0, 100) : [...appInvites]
return
} }
$: filterInvites(query) filteredInvites = appInvites.filter(invite => {
const inviteInfo = invite.info?.apps
if (!query && inviteInfo && prodAppId) {
return Object.keys(inviteInfo).includes(prodAppId)
}
return invite.email.includes(query)
})
}
$: filterByAppAccess, prodAppId, filterInvites(query)
$: if (searchFocus === true) {
filterByAppAccess = false
}
const usersFetch = fetchData({ const usersFetch = fetchData({
API, API,
@ -79,9 +99,9 @@
} }
await usersFetch.update({ await usersFetch.update({
query: { query: {
appId: query ? null : prodAppId, appId: query || !filterByAppAccess ? null : prodAppId,
email: query, email: query,
paginated: query ? null : false, paginated: query || !filterByAppAccess ? null : false,
}, },
}) })
await usersFetch.refresh() await usersFetch.refresh()
@ -107,7 +127,12 @@
} }
const debouncedUpdateFetch = Utils.debounce(searchUsers, 250) const debouncedUpdateFetch = Utils.debounce(searchUsers, 250)
$: debouncedUpdateFetch(query, $store.builderSidePanel, loaded) $: debouncedUpdateFetch(
query,
$store.builderSidePanel,
loaded,
filterByAppAccess
)
const updateAppUser = async (user, role) => { const updateAppUser = async (user, role) => {
if (!prodAppId) { if (!prodAppId) {
@ -182,7 +207,8 @@
} }
const searchGroups = (userGroups, query) => { const searchGroups = (userGroups, query) => {
let filterGroups = query?.length let filterGroups =
query?.length || !filterByAppAccess
? userGroups ? userGroups
: getAppGroups(userGroups, prodAppId) : getAppGroups(userGroups, prodAppId)
return filterGroups return filterGroups
@ -214,7 +240,7 @@
} }
// Adds the 'role' attribute and sets it to the current app. // Adds the 'role' attribute and sets it to the current app.
$: enrichedGroups = getEnrichedGroups($groups) $: enrichedGroups = getEnrichedGroups($groups, filterByAppAccess)
$: filteredGroups = searchGroups(enrichedGroups, query) $: filteredGroups = searchGroups(enrichedGroups, query)
$: groupUsers = buildGroupUsers(filteredGroups, filteredUsers) $: groupUsers = buildGroupUsers(filteredGroups, filteredUsers)
$: allUsers = [...filteredUsers, ...groupUsers] $: allUsers = [...filteredUsers, ...groupUsers]
@ -226,7 +252,7 @@
specific roles for the app. specific roles for the app.
*/ */
const buildGroupUsers = (userGroups, filteredUsers) => { const buildGroupUsers = (userGroups, filteredUsers) => {
if (query) { if (query || !filterByAppAccess) {
return [] return []
} }
// Must exclude users who have explicit privileges // Must exclude users who have explicit privileges
@ -321,12 +347,12 @@
[prodAppId]: role, [prodAppId]: role,
}, },
}) })
await filterInvites() await filterInvites(query)
} }
const onUninviteAppUser = async invite => { const onUninviteAppUser = async invite => {
await uninviteAppUser(invite) await uninviteAppUser(invite)
await filterInvites() await filterInvites(query)
} }
// Purge only the app from the invite or recind the invite if only 1 app remains? // Purge only the app from the invite or recind the invite if only 1 app remains?
@ -351,7 +377,6 @@
onMount(() => { onMount(() => {
rendered = true rendered = true
searchFocus = true
}) })
function handleKeyDown(evt) { function handleKeyDown(evt) {
@ -417,7 +442,6 @@
autocomplete="off" autocomplete="off"
disabled={inviting} disabled={inviting}
value={query} value={query}
autofocus
on:input={e => { on:input={e => {
query = e.target.value.trim() query = e.target.value.trim()
}} }}
@ -428,16 +452,20 @@
<span <span
class="search-input-icon" class="search-input-icon"
class:searching={query} class:searching={query || !filterByAppAccess}
on:click={() => { on:click={() => {
if (!filterByAppAccess) {
filterByAppAccess = true
}
if (!query) { if (!query) {
return return
} }
query = null query = null
userOnboardResponse = null userOnboardResponse = null
filterByAppAccess = true
}} }}
> >
<Icon name={query ? "Close" : "Search"} /> <Icon name={!filterByAppAccess || query ? "Close" : "Search"} />
</span> </span>
</div> </div>
@ -696,7 +724,7 @@
max-width: calc(100vw - 40px); max-width: calc(100vw - 40px);
background: var(--background); background: var(--background);
border-left: var(--border-light); border-left: var(--border-light);
z-index: 3; z-index: 999;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow-y: auto; overflow-y: auto;

View File

@ -35,7 +35,7 @@
} }
</script> </script>
<Panel title={$selectedLayout?.name} icon="Experience" borderLeft> <Panel title={$selectedLayout?.name} icon="Experience" borderLeft wide>
<Layout paddingX="L" paddingY="XL" gap="S"> <Layout paddingX="L" paddingY="XL" gap="S">
<Banner type="warning" showCloseButton={false}> <Banner type="warning" showCloseButton={false}>
Custom layouts are being deprecated. They will be removed in a future Custom layouts are being deprecated. They will be removed in a future

View File

@ -9,7 +9,7 @@
} }
</script> </script>
<Panel borderLeft title="Navigation" icon="InfoOutline"> <Panel borderLeft title="Navigation" icon="InfoOutline" wide>
<Layout paddingX="L" paddingY="XL" gap="S"> <Layout paddingX="L" paddingY="XL" gap="S">
{#if $selectedScreen.layoutId} {#if $selectedScreen.layoutId}
<Banner <Banner

View File

@ -59,6 +59,7 @@
text={screen.routing.route} text={screen.routing.route}
on:click={() => store.actions.screens.select(screen._id)} on:click={() => store.actions.screens.select(screen._id)}
rightAlignIcon rightAlignIcon
showTooltip
> >
<ScreenDropdownMenu screenId={screen._id} /> <ScreenDropdownMenu screenId={screen._id} />
<RoleIndicator slot="right" roleId={screen.routing.roleId} /> <RoleIndicator slot="right" roleId={screen.routing.roleId} />

View File

@ -149,6 +149,7 @@
title={$selectedScreen.routing.route} title={$selectedScreen.routing.route}
icon={$selectedScreen.routing.route === "/" ? "Home" : "WebPage"} icon={$selectedScreen.routing.route === "/" ? "Home" : "WebPage"}
borderLeft borderLeft
wide
> >
<Layout gap="S" paddingX="L" paddingY="XL"> <Layout gap="S" paddingX="L" paddingY="XL">
{#if $selectedScreen.layoutId} {#if $selectedScreen.layoutId}

View File

@ -3,7 +3,7 @@
import { Body, Layout } from "@budibase/bbui" import { Body, Layout } from "@budibase/bbui"
</script> </script>
<Panel borderLeft title="Theme" icon="InfoOutline"> <Panel borderLeft title="Theme" icon="InfoOutline" wide>
<Layout paddingX="L" paddingY="XL"> <Layout paddingX="L" paddingY="XL">
<Body size="S"> <Body size="S">
Your theme is set across all the screens within your app. Your theme is set across all the screens within your app.

View File

@ -133,7 +133,7 @@
</Body> </Body>
</Layout> </Layout>
<Divider /> <Divider />
{#if $licensing.usageMetrics?.dayPasses >= 100} {#if $licensing.usageMetrics?.dayPasses >= 100 || $licensing.errUserLimit}
<div> <div>
<Layout gap="S" justifyItems="center"> <Layout gap="S" justifyItems="center">
<img class="spaceman" alt="spaceman" src={Spaceman} /> <img class="spaceman" alt="spaceman" src={Spaceman} />

View File

@ -43,12 +43,18 @@
} }
$: quotaUsage = $licensing.quotaUsage $: quotaUsage = $licensing.quotaUsage
$: license = $auth.user?.license $: license = $auth.user?.license
$: plan = license?.plan
$: usesInvoicing = plan?.usesInvoicing
$: accountPortalAccess = $auth?.user?.accountPortalAccess $: accountPortalAccess = $auth?.user?.accountPortalAccess
$: quotaReset = quotaUsage?.quotaReset $: quotaReset = quotaUsage?.quotaReset
$: canManagePlan = $: canManagePlan =
($admin.cloud && accountPortalAccess) || (!$admin.cloud && $auth.isAdmin) ($admin.cloud && accountPortalAccess) || (!$admin.cloud && $auth.isAdmin)
$: showButton = !usesInvoicing && accountPortalAccess
const setMonthlyUsage = () => { const setMonthlyUsage = () => {
monthlyUsage = [] monthlyUsage = []
if (quotaUsage.monthly) { if (quotaUsage.monthly) {
@ -121,7 +127,7 @@
const setTextRows = () => { const setTextRows = () => {
textRows = [] textRows = []
if (cancelAt) { if (cancelAt && !usesInvoicing) {
textRows.push({ message: "Subscription has been cancelled" }) textRows.push({ message: "Subscription has been cancelled" })
textRows.push({ textRows.push({
message: `${getDaysRemaining(cancelAt)} days remaining`, message: `${getDaysRemaining(cancelAt)} days remaining`,
@ -213,7 +219,7 @@
description="YOUR CURRENT PLAN" description="YOUR CURRENT PLAN"
title={planTitle()} title={planTitle()}
{primaryActionText} {primaryActionText}
primaryAction={accountPortalAccess ? goToAccountPortal : undefined} primaryAction={showButton ? goToAccountPortal : undefined}
{textRows} {textRows}
> >
<div class="content"> <div class="content">
@ -224,12 +230,6 @@
<Usage {usage} warnWhenFull={WARN_USAGE.includes(usage.name)} /> <Usage {usage} warnWhenFull={WARN_USAGE.includes(usage.name)} />
</div> </div>
{/each} {/each}
</Layout>
</div>
{#if monthlyUsage.length}
<div class="column">
<Layout noPadding gap="M">
<Layout gap="XS" noPadding> <Layout gap="XS" noPadding>
<Heading size="S">Monthly limits</Heading> <Heading size="S">Monthly limits</Heading>
<div class="detail"> <div class="detail">
@ -242,15 +242,11 @@
</Layout> </Layout>
<Layout noPadding gap="M"> <Layout noPadding gap="M">
{#each monthlyUsage as usage} {#each monthlyUsage as usage}
<Usage <Usage {usage} warnWhenFull={WARN_USAGE.includes(usage.name)} />
{usage}
warnWhenFull={WARN_USAGE.includes(usage.name)}
/>
{/each} {/each}
</Layout> </Layout>
</Layout> </Layout>
</div> </div>
{/if}
</div> </div>
</DashCard> </DashCard>
</Layout> </Layout>

View File

@ -14,6 +14,7 @@
import Spinner from "components/common/Spinner.svelte" import Spinner from "components/common/Spinner.svelte"
import CreateAppModal from "components/start/CreateAppModal.svelte" import CreateAppModal from "components/start/CreateAppModal.svelte"
import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte" import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte"
import AccountLockedModal from "components/portal/licensing/AccountLockedModal.svelte"
import { store, automationStore } from "builderStore" import { store, automationStore } from "builderStore"
import { API } from "api" import { API } from "api"
@ -28,6 +29,7 @@
let template let template
let creationModal let creationModal
let appLimitModal let appLimitModal
let accountLockedModal
let creatingApp = false let creatingApp = false
let searchTerm = "" let searchTerm = ""
let creatingFromTemplate = false let creatingFromTemplate = false
@ -48,6 +50,11 @@
: true) : true)
) )
$: automationErrors = getAutomationErrors(enrichedApps) $: automationErrors = getAutomationErrors(enrichedApps)
$: isOwner = $auth.accountPortalAccess && $admin.cloud
const usersLimitLockAction = $licensing?.errUserLimit
? () => accountLockedModal.show()
: null
const enrichApps = (apps, user, sortBy) => { const enrichApps = (apps, user, sortBy) => {
const enrichedApps = apps.map(app => ({ const enrichedApps = apps.map(app => ({
@ -189,6 +196,9 @@
creatingFromTemplate = true creatingFromTemplate = true
createAppFromTemplateUrl(initInfo.init_template) createAppFromTemplateUrl(initInfo.init_template)
} }
if (usersLimitLockAction) {
usersLimitLockAction()
}
} catch (error) { } catch (error) {
notifications.error("Error getting init info") notifications.error("Error getting init info")
} }
@ -230,20 +240,30 @@
<Layout noPadding gap="L"> <Layout noPadding gap="L">
<div class="title"> <div class="title">
<div class="buttons"> <div class="buttons">
<Button size="M" cta on:click={initiateAppCreation}> <Button
size="M"
cta
on:click={usersLimitLockAction || initiateAppCreation}
>
Create new app Create new app
</Button> </Button>
{#if $apps?.length > 0} {#if $apps?.length > 0}
<Button <Button
size="M" size="M"
secondary secondary
on:click={$goto("/builder/portal/apps/templates")} on:click={usersLimitLockAction ||
$goto("/builder/portal/apps/templates")}
> >
View templates View templates
</Button> </Button>
{/if} {/if}
{#if !$apps?.length} {#if !$apps?.length}
<Button size="L" quiet secondary on:click={initiateAppImport}> <Button
size="L"
quiet
secondary
on:click={usersLimitLockAction || initiateAppImport}
>
Import app Import app
</Button> </Button>
{/if} {/if}
@ -267,7 +287,7 @@
<div class="app-table"> <div class="app-table">
{#each filteredApps as app (app.appId)} {#each filteredApps as app (app.appId)}
<AppRow {app} /> <AppRow {app} lockedAction={usersLimitLockAction} />
{/each} {/each}
</div> </div>
</Layout> </Layout>
@ -294,6 +314,11 @@
</Modal> </Modal>
<AppLimitModal bind:this={appLimitModal} /> <AppLimitModal bind:this={appLimitModal} />
<AccountLockedModal
bind:this={accountLockedModal}
onConfirm={() =>
isOwner ? $licensing.goToUpgradePage() : $licensing.goToPricingPage()}
/>
<style> <style>
.title { .title {

View File

@ -107,8 +107,9 @@
useSampleData, useSampleData,
isGoogle, isGoogle,
}) => { }) => {
let app
try { try {
const app = await createApp(useSampleData) app = await createApp(useSampleData)
let datasource let datasource
if (datasourceConfig) { if (datasourceConfig) {
@ -134,6 +135,17 @@
console.log(e) console.log(e)
creationLoading = false creationLoading = false
notifications.error("There was a problem creating your app") notifications.error("There was a problem creating your app")
// Reset the store so that we don't send up stale headers
store.actions.reset()
// If we successfully created an app, delete it again so that we
// can try again once the error has been corrected.
// This also ensures onboarding can't be skipped by entering invalid
// data credentials.
if (app?.appId) {
await API.deleteApp(app.appId)
}
} }
} }
</script> </script>
@ -146,6 +158,7 @@
/> />
</Modal> </Modal>
<div class="full-width">
<SplitPage> <SplitPage>
{#if stage === "name"} {#if stage === "name"}
<NamePanel bind:name bind:url onNext={() => (stage = "data")} /> <NamePanel bind:name bind:url onNext={() => (stage = "data")} />
@ -163,7 +176,9 @@
{:else if stage === "data"} {:else if stage === "data"}
<DataPanel onBack={() => (stage = "name")}> <DataPanel onBack={() => (stage = "name")}>
<div class="dataButton"> <div class="dataButton">
<FancyButton on:click={() => handleCreateApp({ useSampleData: true })}> <FancyButton
on:click={() => handleCreateApp({ useSampleData: true })}
>
<div class="dataButtonContent"> <div class="dataButtonContent">
<div class="dataButtonIcon"> <div class="dataButtonIcon">
<img <img
@ -218,8 +233,12 @@
<ExampleApp {name} showData={stage !== "name"} /> <ExampleApp {name} showData={stage !== "name"} />
</div> </div>
</SplitPage> </SplitPage>
</div>
<style> <style>
.full-width {
width: 100%;
}
.centered { .centered {
display: flex; display: flex;
justify-content: center; justify-content: center;

View File

@ -176,7 +176,7 @@
<Heading>Backups</Heading> <Heading>Backups</Heading>
{#if !$licensing.backupsEnabled} {#if !$licensing.backupsEnabled}
<Tags> <Tags>
<Tag icon="LockClosed">Pro plan</Tag> <Tag icon="LockClosed">Premium</Tag>
</Tags> </Tags>
{/if} {/if}
</div> </div>

View File

@ -13,7 +13,6 @@
notifications, notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import { store } from "builderStore" import { store } from "builderStore"
import clientPackage from "@budibase/client/package.json"
import { processStringSync } from "@budibase/string-templates" import { processStringSync } from "@budibase/string-templates"
import { users, auth, apps, groups, overview } from "stores/portal" import { users, auth, apps, groups, overview } from "stores/portal"
import { fetchData } from "@budibase/frontend-core" import { fetchData } from "@budibase/frontend-core"
@ -40,7 +39,7 @@
}, },
}, },
}) })
$: updateAvailable = clientPackage.version !== $store.version $: updateAvailable = $store.upgradableVersion !== $store.version
$: isPublished = app?.status === AppStatus.DEPLOYED $: isPublished = app?.status === AppStatus.DEPLOYED
$: appEditorId = !app?.updatedBy ? $auth.user._id : app?.updatedBy $: appEditorId = !app?.updatedBy ? $auth.user._id : app?.updatedBy
$: appEditorText = appEditor?.firstName || appEditor?.email $: appEditorText = appEditor?.firstName || appEditor?.email
@ -172,8 +171,8 @@
<Heading size="XS">{$store.version}</Heading> <Heading size="XS">{$store.version}</Heading>
{#if updateAvailable} {#if updateAvailable}
<div class="version-status"> <div class="version-status">
New version <strong>{clientPackage.version}</strong> is available New version <strong>{$store.upgradableVersion}</strong> is
- available -
<Link <Link
on:click={() => { on:click={() => {
$goto("./version") $goto("./version")

View File

@ -1,12 +1,11 @@
<script> <script>
import { Layout, Heading, Body, Divider, Button } from "@budibase/bbui" import { Layout, Heading, Body, Divider, Button } from "@budibase/bbui"
import { store } from "builderStore" import { store } from "builderStore"
import clientPackage from "@budibase/client/package.json"
import VersionModal from "components/deploy/VersionModal.svelte" import VersionModal from "components/deploy/VersionModal.svelte"
let versionModal let versionModal
$: updateAvailable = clientPackage.version !== $store.version $: updateAvailable = $store.upgradableVersion !== $store.version
</script> </script>
<Layout noPadding> <Layout noPadding>
@ -18,7 +17,7 @@
{#if updateAvailable} {#if updateAvailable}
<Body> <Body>
The app is currently using version <strong>{$store.version}</strong> The app is currently using version <strong>{$store.version}</strong>
but version <strong>{clientPackage.version}</strong> is available. but version <strong>{$store.upgradableVersion}</strong> is available.
<br /> <br />
Updates can contain new features, performance improvements and bug fixes. Updates can contain new features, performance improvements and bug fixes.
</Body> </Body>

View File

@ -378,7 +378,7 @@
</div> </div>
{#if !$licensing.enforceableSSO} {#if !$licensing.enforceableSSO}
<Tags> <Tags>
<Tag icon="LockClosed">Enterprise plan</Tag> <Tag icon="LockClosed">Enterprise</Tag>
</Tags> </Tags>
{/if} {/if}
</div> </div>

View File

@ -213,7 +213,7 @@
{/if} {/if}
{#if isCloud && !brandingEnabled} {#if isCloud && !brandingEnabled}
<Tags> <Tags>
<Tag icon="LockClosed">Pro</Tag> <Tag icon="LockClosed">Premium</Tag>
</Tags> </Tags>
{/if} {/if}
</div> </div>

View File

@ -1,47 +1,28 @@
<script> <script>
import { url, goto } from "@roxi/routify"
import { import {
Button, ActionMenu,
Layout,
Heading, Heading,
Icon, Icon,
Popover, Layout,
notifications,
Table,
ActionMenu,
MenuItem, MenuItem,
Modal, Modal,
Table,
notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import UserGroupPicker from "components/settings/UserGroupPicker.svelte" import { goto, url } from "@roxi/routify"
import { createPaginationStore } from "helpers/pagination"
import { users, apps, groups, auth, features } from "stores/portal"
import { onMount, setContext } from "svelte"
import { roles } from "stores/backend"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { Breadcrumb, Breadcrumbs } from "components/portal/page"
import { roles } from "stores/backend"
import { apps, auth, features, groups } from "stores/portal"
import { onMount, setContext } from "svelte"
import AppNameTableRenderer from "../users/_components/AppNameTableRenderer.svelte"
import AppRoleTableRenderer from "../users/_components/AppRoleTableRenderer.svelte"
import CreateEditGroupModal from "./_components/CreateEditGroupModal.svelte" import CreateEditGroupModal from "./_components/CreateEditGroupModal.svelte"
import GroupIcon from "./_components/GroupIcon.svelte" import GroupIcon from "./_components/GroupIcon.svelte"
import { Breadcrumbs, Breadcrumb } from "components/portal/page" import GroupUsers from "./_components/GroupUsers.svelte"
import AppNameTableRenderer from "../users/_components/AppNameTableRenderer.svelte"
import RemoveUserTableRenderer from "./_components/RemoveUserTableRenderer.svelte"
import AppRoleTableRenderer from "../users/_components/AppRoleTableRenderer.svelte"
import ScimBanner from "../_components/SCIMBanner.svelte"
export let groupId export let groupId
$: userSchema = {
email: {
width: "1fr",
},
...(readonly
? {}
: {
_id: {
displayName: "",
width: "auto",
borderLeft: true,
},
}),
}
const appSchema = { const appSchema = {
name: { name: {
width: "2fr", width: "2fr",
@ -50,12 +31,6 @@
width: "1fr", width: "1fr",
}, },
} }
const customUserTableRenderers = [
{
column: "_id",
component: RemoveUserTableRenderer,
},
]
const customAppTableRenderers = [ const customAppTableRenderers = [
{ {
column: "name", column: "name",
@ -67,20 +42,12 @@
}, },
] ]
let popoverAnchor
let popover
let searchTerm = ""
let prevSearch = undefined
let pageInfo = createPaginationStore()
let loaded = false let loaded = false
let editModal, deleteModal let editModal, deleteModal
$: scimEnabled = $features.isScimEnabled $: scimEnabled = $features.isScimEnabled
$: readonly = !$auth.isAdmin || scimEnabled $: readonly = !$auth.isAdmin || scimEnabled
$: page = $pageInfo.page
$: fetchUsers(page, searchTerm)
$: group = $groups.find(x => x._id === groupId) $: group = $groups.find(x => x._id === groupId)
$: filtered = $users.data
$: groupApps = $apps $: groupApps = $apps
.filter(app => .filter(app =>
groups.actions groups.actions
@ -97,25 +64,6 @@
} }
} }
async function fetchUsers(page, search) {
if ($pageInfo.loading) {
return
}
// need to remove the page if they've started searching
if (search && !prevSearch) {
pageInfo.reset()
page = undefined
}
prevSearch = search
try {
pageInfo.loading()
await users.search({ page, email: search })
pageInfo.fetched($users.hasNextPage, $users.nextPage)
} catch (error) {
notifications.error("Error getting user list")
}
}
async function deleteGroup() { async function deleteGroup() {
try { try {
await groups.actions.delete(group) await groups.actions.delete(group)
@ -130,21 +78,17 @@
try { try {
await groups.actions.save(group) await groups.actions.save(group)
} catch (error) { } catch (error) {
if (error.message) {
notifications.error(error.message)
} else {
notifications.error(`Failed to save user group`) notifications.error(`Failed to save user group`)
} }
} }
const removeUser = async id => {
await groups.actions.removeUser(groupId, id)
} }
const removeApp = async app => { const removeApp = async app => {
await groups.actions.removeApp(groupId, apps.getProdAppID(app.devId)) await groups.actions.removeApp(groupId, apps.getProdAppID(app.devId))
} }
setContext("users", {
removeUser,
})
setContext("roles", { setContext("roles", {
updateRole: () => {}, updateRole: () => {},
removeRole: removeApp, removeRole: removeApp,
@ -186,41 +130,7 @@
</div> </div>
<Layout noPadding gap="S"> <Layout noPadding gap="S">
<div class="header"> <GroupUsers {groupId} />
<Heading size="S">Users</Heading>
{#if !scimEnabled}
<div bind:this={popoverAnchor}>
<Button disabled={readonly} on:click={popover.show()} cta
>Add user</Button
>
</div>
{:else}
<ScimBanner />
{/if}
<Popover align="right" bind:this={popover} anchor={popoverAnchor}>
<UserGroupPicker
bind:searchTerm
labelKey="email"
selected={group.users?.map(user => user._id)}
list={$users.data}
on:select={e => groups.actions.addUser(groupId, e.detail)}
on:deselect={e => groups.actions.removeUser(groupId, e.detail)}
/>
</Popover>
</div>
<Table
schema={userSchema}
data={group?.users}
allowEditRows={false}
customPlaceholder
customRenderers={customUserTableRenderers}
on:click={e => $goto(`../users/${e.detail._id}`)}
>
<div class="placeholder" slot="placeholder">
<Heading size="S">This user group doesn't have any users</Heading>
</div>
</Table>
</Layout> </Layout>
<Layout noPadding gap="S"> <Layout noPadding gap="S">

View File

@ -9,15 +9,23 @@
export let group export let group
export let saveGroup export let saveGroup
let nameError
</script> </script>
<ModalContent <ModalContent
onConfirm={() => saveGroup(group)} onConfirm={() => {
if (!group.name?.trim()) {
nameError = "Group name cannot be empty"
return false
}
saveGroup(group)
}}
size="M" size="M"
title={group?._rev ? "Edit group" : "Create group"} title={group?._rev ? "Edit group" : "Create group"}
confirmText="Save" confirmText="Save"
> >
<Input bind:value={group.name} label="Name" /> <Input bind:value={group.name} label="Name" error={nameError} />
<div class="modal-format"> <div class="modal-format">
<div class="modal-inner"> <div class="modal-inner">
<Body size="XS">Icon</Body> <Body size="XS">Icon</Body>

View File

@ -0,0 +1,59 @@
<script>
import { Button, Popover, notifications } from "@budibase/bbui"
import UserGroupPicker from "components/settings/UserGroupPicker.svelte"
import { createPaginationStore } from "helpers/pagination"
import { auth, groups, users } from "stores/portal"
export let groupId
export let onUsersUpdated
let popoverAnchor
let popover
let searchTerm = ""
let prevSearch = undefined
let pageInfo = createPaginationStore()
$: readonly = !$auth.isAdmin
$: page = $pageInfo.page
$: searchUsers(page, searchTerm)
$: group = $groups.find(x => x._id === groupId)
async function searchUsers(page, search) {
if ($pageInfo.loading) {
return
}
// need to remove the page if they've started searching
if (search && !prevSearch) {
pageInfo.reset()
page = undefined
}
prevSearch = search
try {
pageInfo.loading()
await users.search({ page, email: search })
pageInfo.fetched($users.hasNextPage, $users.nextPage)
} catch (error) {
notifications.error("Error getting user list")
}
}
</script>
<div bind:this={popoverAnchor}>
<Button disabled={readonly} on:click={popover.show()} cta>Add user</Button>
</div>
<Popover align="right" bind:this={popover} anchor={popoverAnchor}>
<UserGroupPicker
bind:searchTerm
labelKey="email"
selected={group.users?.map(user => user._id)}
list={$users.data}
on:select={async e => {
await groups.actions.addUser(groupId, e.detail)
onUsersUpdated()
}}
on:deselect={async e => {
await groups.actions.removeUser(groupId, e.detail)
onUsersUpdated()
}}
/>
</Popover>

View File

@ -0,0 +1,133 @@
<script>
import EditUserPicker from "./EditUserPicker.svelte"
import { Heading, Pagination, Table, Search } from "@budibase/bbui"
import { fetchData } from "@budibase/frontend-core"
import { goto } from "@roxi/routify"
import { API } from "api"
import { auth, features, groups } from "stores/portal"
import { setContext } from "svelte"
import ScimBanner from "../../_components/SCIMBanner.svelte"
import RemoveUserTableRenderer from "../_components/RemoveUserTableRenderer.svelte"
export let groupId
let emailSearch
let fetchGroupUsers
$: fetchGroupUsers = fetchData({
API,
datasource: {
type: "groupUser",
},
options: {
query: {
groupId,
emailSearch,
},
},
})
$: userSchema = {
email: {
width: "1fr",
},
...(readonly
? {}
: {
_id: {
displayName: "",
width: "auto",
borderLeft: true,
},
}),
}
const customUserTableRenderers = [
{
column: "_id",
component: RemoveUserTableRenderer,
},
]
$: scimEnabled = $features.isScimEnabled
$: readonly = !$auth.isAdmin || scimEnabled
const removeUser = async id => {
await groups.actions.removeUser(groupId, id)
fetchGroupUsers.refresh()
}
setContext("users", {
removeUser,
})
</script>
<div class="header">
{#if !scimEnabled}
<EditUserPicker {groupId} onUsersUpdated={fetchGroupUsers.getInitialData} />
{:else}
<ScimBanner />
{/if}
<div class="controls-right">
<Search bind:value={emailSearch} placeholder="Search email" />
</div>
</div>
<Table
schema={userSchema}
data={$fetchGroupUsers?.rows}
loading={$fetchGroupUsers.loading}
allowEditRows={false}
customPlaceholder
customRenderers={customUserTableRenderers}
on:click={e => $goto(`../users/${e.detail._id}`)}
>
<div class="placeholder" slot="placeholder">
<Heading size="S"
>{emailSearch
? `No users found matching the email "${emailSearch}"`
: "This user group doesn't have any users"}</Heading
>
</div>
</Table>
<div class="pagination">
<Pagination
page={$fetchGroupUsers.pageNumber + 1}
hasPrevPage={$fetchGroupUsers.loading
? false
: $fetchGroupUsers.hasPrevPage}
hasNextPage={$fetchGroupUsers.loading
? false
: $fetchGroupUsers.hasNextPage}
goToPrevPage={$fetchGroupUsers.loading ? null : fetchGroupUsers.prevPage}
goToNextPage={$fetchGroupUsers.loading ? null : fetchGroupUsers.nextPage}
/>
</div>
<style>
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: var(--spacing-l);
}
.header :global(.spectrum-Heading) {
flex: 1 1 auto;
}
.placeholder {
width: 100%;
text-align: center;
}
.controls-right {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
gap: var(--spacing-xl);
}
.controls-right :global(.spectrum-Search) {
width: 200px;
}
</style>

View File

@ -66,6 +66,8 @@
} catch (error) { } catch (error) {
if (error.status === 400) { if (error.status === 400) {
notifications.error(error.message) notifications.error(error.message)
} else if (error.message) {
notifications.error(error.message)
} else { } else {
notifications.error(`Failed to save group`) notifications.error(`Failed to save group`)
} }
@ -94,7 +96,7 @@
<Heading size="M">Groups</Heading> <Heading size="M">Groups</Heading>
{#if !$licensing.groupsEnabled} {#if !$licensing.groupsEnabled}
<Tags> <Tags>
<Tag icon="LockClosed">Pro plan</Tag> <Tag icon="LockClosed">Business</Tag>
</Tags> </Tags>
{/if} {/if}
</div> </div>

View File

@ -30,8 +30,8 @@
$: hasError = userData.find(x => x.error != null) $: hasError = userData.find(x => x.error != null)
$: userCount = $licensing.userCount + userData.length $: userCount = $licensing.userCount + userData.length
$: willReach = licensing.willReachUserLimit(userCount) $: reached = licensing.usersLimitReached(userCount)
$: willExceed = licensing.willExceedUserLimit(userCount) $: exceeded = licensing.usersLimitExceeded(userCount)
function removeInput(idx) { function removeInput(idx) {
userData = userData.filter((e, i) => i !== idx) userData = userData.filter((e, i) => i !== idx)
@ -87,7 +87,7 @@
confirmDisabled={disabled} confirmDisabled={disabled}
cancelText="Cancel" cancelText="Cancel"
showCloseIcon={false} showCloseIcon={false}
disabled={hasError || !userData.length || willExceed} disabled={hasError || !userData.length || exceeded}
> >
<Layout noPadding gap="XS"> <Layout noPadding gap="XS">
<Label>Email address</Label> <Label>Email address</Label>
@ -118,7 +118,7 @@
</div> </div>
{/each} {/each}
{#if willReach} {#if reached}
<div class="user-notification"> <div class="user-notification">
<Icon name="Info" /> <Icon name="Info" />
<span> <span>

View File

@ -25,10 +25,10 @@
$: invalidEmails = [] $: invalidEmails = []
$: userCount = $licensing.userCount + userEmails.length $: userCount = $licensing.userCount + userEmails.length
$: willExceed = userCount > $licensing.userLimit $: exceed = licensing.usersLimitExceeded(userCount)
$: importDisabled = $: importDisabled =
!userEmails.length || !validEmails(userEmails) || !usersRole || willExceed !userEmails.length || !validEmails(userEmails) || !usersRole || exceed
const validEmails = userEmails => { const validEmails = userEmails => {
if ($admin.cloud && userEmails.length > MAX_USERS_UPLOAD_LIMIT) { if ($admin.cloud && userEmails.length > MAX_USERS_UPLOAD_LIMIT) {
@ -93,7 +93,7 @@
</label> </label>
</div> </div>
{#if willExceed} {#if exceed}
<div class="user-notification"> <div class="user-notification">
<Icon name="Info" /> <Icon name="Info" />
{capitalise($licensing.license.plan.type)} plan is limited to {$licensing.userLimit} {capitalise($licensing.license.plan.type)} plan is limited to {$licensing.userLimit}

View File

@ -88,6 +88,16 @@
}, },
} }
const getPendingSchema = tblSchema => {
if (!tblSchema) {
return {}
}
let pendingSchema = JSON.parse(JSON.stringify(tblSchema))
pendingSchema.email.displayName = "Pending Invites"
return pendingSchema
}
$: pendingSchema = getPendingSchema(schema)
$: userData = [] $: userData = []
$: inviteUsersResponse = { successful: [], unsuccessful: [] } $: inviteUsersResponse = { successful: [], unsuccessful: [] }
$: { $: {
@ -110,6 +120,24 @@
} }
}) })
} }
let invitesLoaded = false
let pendingInvites = []
let parsedInvites = []
const invitesToSchema = invites => {
return invites.map(invite => {
const { admin, builder, userGroups, apps } = invite.info
return {
email: invite.email,
builder,
admin,
userGroups: userGroups,
apps: apps ? [...new Set(Object.keys(apps))] : undefined,
}
})
}
$: parsedInvites = invitesToSchema(pendingInvites)
const updateFetch = email => { const updateFetch = email => {
fetch.update({ fetch.update({
@ -144,6 +172,7 @@
})) }))
try { try {
inviteUsersResponse = await users.invite(payload) inviteUsersResponse = await users.invite(payload)
pendingInvites = await users.getInvites()
inviteConfirmationModal.show() inviteConfirmationModal.show()
} catch (error) { } catch (error) {
notifications.error("Error inviting user") notifications.error("Error inviting user")
@ -232,12 +261,13 @@
try { try {
await groups.actions.init() await groups.actions.init()
groupsLoaded = true groupsLoaded = true
pendingInvites = await users.getInvites()
invitesLoaded = true
} catch (error) { } catch (error) {
notifications.error("Error fetching user group data") notifications.error("Error fetching user group data")
} }
}) })
let staticUserLimit = $licensing.license.quotas.usage.static.users.value
</script> </script>
<Layout noPadding gap="M"> <Layout noPadding gap="M">
@ -246,7 +276,7 @@
<Body>Add users and control who gets access to your published apps</Body> <Body>Add users and control who gets access to your published apps</Body>
</Layout> </Layout>
<Divider /> <Divider />
{#if $licensing.warnUserLimit} {#if $licensing.errUserLimit}
<InlineAlert <InlineAlert
type="error" type="error"
onConfirm={() => { onConfirm={() => {
@ -258,13 +288,9 @@
}} }}
buttonText={isOwner ? "Upgrade" : "View plans"} buttonText={isOwner ? "Upgrade" : "View plans"}
cta cta
header={`Users will soon be limited to ${staticUserLimit}`} header="Account de-activated"
message={`Our free plan is going to be limited to ${staticUserLimit} users in ${$licensing.userLimitDays}. message="Due to the free plan user limit being exceeded, your account has been de-activated.
Upgrade your plan to re-activate your account."
This means any users exceeding the limit have been de-activated.
De-activated users will not able to access the builder or any published apps until you upgrade to one of our paid plans.
`}
/> />
{/if} {/if}
<div class="controls"> <div class="controls">
@ -324,6 +350,15 @@
goToNextPage={fetch.nextPage} goToNextPage={fetch.nextPage}
/> />
</div> </div>
<Table
schema={pendingSchema}
data={parsedInvites}
allowEditColumns={false}
allowEditRows={false}
{customRenderers}
loading={!invitesLoaded}
allowClickRows={false}
/>
</Layout> </Layout>
<Modal bind:this={createUserModal}> <Modal bind:this={createUserModal}>

View File

@ -25,6 +25,8 @@ export function createDatasourcesStore() {
store.update(state => ({ store.update(state => ({
...state, ...state,
selectedDatasourceId: id, selectedDatasourceId: id,
// Remove any possible schema error
schemaError: null,
})) }))
} }

View File

@ -28,7 +28,7 @@ export function createGroupsStore() {
// on the backend anyway // on the backend anyway
if (get(licensing).groupsEnabled) { if (get(licensing).groupsEnabled) {
const groups = await API.getGroups() const groups = await API.getGroups()
store.set(groups) store.set(groups.data)
} }
}, },

View File

@ -4,7 +4,7 @@ import { auth, admin } from "stores/portal"
import { Constants } from "@budibase/frontend-core" import { Constants } from "@budibase/frontend-core"
import { StripeStatus } from "components/portal/licensing/constants" import { StripeStatus } from "components/portal/licensing/constants"
import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags" import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags"
import dayjs from "dayjs" import { PlanModel } from "@budibase/types"
const UNLIMITED = -1 const UNLIMITED = -1
@ -12,6 +12,7 @@ export const createLicensingStore = () => {
const DEFAULT = { const DEFAULT = {
// navigation // navigation
goToUpgradePage: () => {}, goToUpgradePage: () => {},
goToPricingPage: () => {},
// the top level license // the top level license
license: undefined, license: undefined,
isFreePlan: true, isFreePlan: true,
@ -37,29 +38,37 @@ export const createLicensingStore = () => {
// user limits // user limits
userCount: undefined, userCount: undefined,
userLimit: undefined, userLimit: undefined,
userLimitDays: undefined,
userLimitReached: false, userLimitReached: false,
warnUserLimit: false, errUserLimit: false,
} }
const oneDayInMilliseconds = 86400000 const oneDayInMilliseconds = 86400000
const store = writable(DEFAULT) const store = writable(DEFAULT)
function willReachUserLimit(userCount, userLimit) { function usersLimitReached(userCount, userLimit) {
if (userLimit === UNLIMITED) { if (userLimit === UNLIMITED) {
return false return false
} }
return userCount >= userLimit return userCount >= userLimit
} }
function willExceedUserLimit(userCount, userLimit) { function usersLimitExceeded(userCount, userLimit) {
if (userLimit === UNLIMITED) { if (userLimit === UNLIMITED) {
return false return false
} }
return userCount > userLimit return userCount > userLimit
} }
async function isCloud() {
let adminStore = get(admin)
if (!adminStore.loaded) {
await admin.init()
adminStore = get(admin)
}
return adminStore.cloud
}
const actions = { const actions = {
init: async () => { init: async () => {
actions.setNavigation() actions.setNavigation()
@ -71,10 +80,14 @@ export const createLicensingStore = () => {
const goToUpgradePage = () => { const goToUpgradePage = () => {
window.location.href = upgradeUrl window.location.href = upgradeUrl
} }
const goToPricingPage = () => {
window.open("https://budibase.com/pricing/", "_blank")
}
store.update(state => { store.update(state => {
return { return {
...state, ...state,
goToUpgradePage, goToUpgradePage,
goToPricingPage,
} }
}) })
}, },
@ -128,15 +141,15 @@ export const createLicensingStore = () => {
quotaUsage, quotaUsage,
} }
}) })
actions.setUsageMetrics() await actions.setUsageMetrics()
}, },
willReachUserLimit: userCount => { usersLimitReached: userCount => {
return willReachUserLimit(userCount, get(store).userLimit) return usersLimitReached(userCount, get(store).userLimit)
}, },
willExceedUserLimit(userCount) { usersLimitExceeded(userCount) {
return willExceedUserLimit(userCount, get(store).userLimit) return usersLimitExceeded(userCount, get(store).userLimit)
}, },
setUsageMetrics: () => { setUsageMetrics: async () => {
if (isEnabled(TENANT_FEATURE_FLAGS.LICENSING)) { if (isEnabled(TENANT_FEATURE_FLAGS.LICENSING)) {
const usage = get(store).quotaUsage const usage = get(store).quotaUsage
const license = get(auth).user.license const license = get(auth).user.license
@ -198,11 +211,13 @@ export const createLicensingStore = () => {
const userQuota = license.quotas.usage.static.users const userQuota = license.quotas.usage.static.users
const userLimit = userQuota?.value const userLimit = userQuota?.value
const userCount = usage.usageQuota.users const userCount = usage.usageQuota.users
const userLimitReached = willReachUserLimit(userCount, userLimit) const userLimitReached = usersLimitReached(userCount, userLimit)
const userLimitExceeded = willExceedUserLimit(userCount, userLimit) const userLimitExceeded = usersLimitExceeded(userCount, userLimit)
const days = dayjs(userQuota?.startDate).diff(dayjs(), "day") const isCloudAccount = await isCloud()
const userLimitDays = days > 1 ? `${days} days` : "1 day" const errUserLimit =
const warnUserLimit = userQuota?.startDate && userLimitExceeded isCloudAccount &&
license.plan.model === PlanModel.PER_USER &&
userLimitExceeded
store.update(state => { store.update(state => {
return { return {
@ -217,9 +232,8 @@ export const createLicensingStore = () => {
// user limits // user limits
userCount, userCount,
userLimit, userLimit,
userLimitDays,
userLimitReached, userLimitReached,
warnUserLimit, errUserLimit,
} }
}) })
} }

View File

@ -41,7 +41,7 @@ export function createUsersStore() {
inviteCode, inviteCode,
password, password,
firstName, firstName,
lastName, lastName: !lastName?.trim() ? undefined : lastName,
}) })
} }
@ -114,8 +114,10 @@ export function createUsersStore() {
const getUserRole = ({ admin, builder }) => const getUserRole = ({ admin, builder }) =>
admin?.global ? "admin" : builder?.global ? "developer" : "appUser" admin?.global ? "admin" : builder?.global ? "developer" : "appUser"
const refreshUsage = fn => async args => { const refreshUsage =
const response = await fn(args) fn =>
async (...args) => {
const response = await fn(...args)
await licensing.setQuotaUsage() await licensing.setQuotaUsage()
return response return response
} }
@ -133,7 +135,7 @@ export function createUsersStore() {
updateInvite, updateInvite,
getUserCountByApp, getUserCountByApp,
// any operation that adds or deletes users // any operation that adds or deletes users
acceptInvite: refreshUsage(acceptInvite), acceptInvite,
create: refreshUsage(create), create: refreshUsage(create),
save: refreshUsage(save), save: refreshUsage(save),
bulkDelete: refreshUsage(bulkDelete), bulkDelete: refreshUsage(bulkDelete),

View File

@ -4,18 +4,9 @@
"composite": true, "composite": true,
"declaration": true, "declaration": true,
"sourceMap": true, "sourceMap": true,
"baseUrl": ".", "baseUrl": "."
"paths": {
"@budibase/types": ["../types/src"],
"@budibase/backend-core": ["../backend-core/src"],
"@budibase/backend-core/*": ["../backend-core/*.js"]
}
}, },
"ts-node": { "ts-node": {
"require": ["tsconfig-paths/register"] "require": ["tsconfig-paths/register"]
}, }
"references": [
{ "path": "../types" },
{ "path": "../backend-core" },
]
} }

View File

@ -1,10 +1,10 @@
{ {
"name": "@budibase/cli", "name": "@budibase/cli",
"version": "2.5.6-alpha.30", "version": "0.0.1",
"description": "Budibase CLI, for developers, self hosting and migrations.", "description": "Budibase CLI, for developers, self hosting and migrations.",
"main": "dist/index.js", "main": "dist/src/index.js",
"bin": { "bin": {
"budi": "dist/index.js" "budi": "dist/src/index.js"
}, },
"author": "Budibase", "author": "Budibase",
"license": "GPL-3.0", "license": "GPL-3.0",
@ -29,14 +29,14 @@
"outputPath": "build" "outputPath": "build"
}, },
"dependencies": { "dependencies": {
"@budibase/backend-core": "2.5.6-alpha.30", "@budibase/backend-core": "0.0.1",
"@budibase/string-templates": "2.5.6-alpha.30", "@budibase/string-templates": "0.0.1",
"@budibase/types": "2.5.6-alpha.30", "@budibase/types": "0.0.1",
"axios": "0.21.2", "axios": "0.21.2",
"chalk": "4.1.0", "chalk": "4.1.0",
"cli-progress": "3.11.2", "cli-progress": "3.11.2",
"commander": "7.1.0", "commander": "7.1.0",
"docker-compose": "0.23.12", "docker-compose": "0.24.0",
"dotenv": "16.0.1", "dotenv": "16.0.1",
"download": "8.0.0", "download": "8.0.0",
"find-free-port": "^2.0.0", "find-free-port": "^2.0.0",

View File

@ -1,19 +1,18 @@
#!/usr/bin/env node #!/usr/bin/env node
import { logging } from "@budibase/backend-core" process.env.DISABLE_PINO_LOGGER = "1"
logging.disableLogger()
import "./prebuilds" import "./prebuilds"
import "./environment" import "./environment"
import { env } from "@budibase/backend-core"
import { getCommands } from "./options" import { getCommands } from "./options"
import { Command } from "commander" import { Command } from "commander"
import { getHelpDescription } from "./utils" import { getHelpDescription } from "./utils"
import { version } from "../package.json"
// add hosting config // add hosting config
async function init() { async function init() {
const program = new Command() const program = new Command()
.addHelpCommand("help", getHelpDescription("Help with Budibase commands.")) .addHelpCommand("help", getHelpDescription("Help with Budibase commands."))
.helpOption(false) .helpOption(false)
.version(env.VERSION) .version(version)
// add commands // add commands
for (let command of getCommands()) { for (let command of getCommands()) {
command.configure(program) command.configure(program)

View File

@ -13,7 +13,7 @@ if (!process.argv[0].includes("node")) {
} }
function checkForBinaries() { function checkForBinaries() {
const readDir = join(__filename, "..", "..", PREBUILDS, ARCH) const readDir = join(__filename, "..", "..", "..", PREBUILDS, ARCH)
if (fs.existsSync(PREBUILD_DIR) || !fs.existsSync(readDir)) { if (fs.existsSync(PREBUILD_DIR) || !fs.existsSync(readDir)) {
return return
} }

Some files were not shown because too many files have changed in this diff Show More