merge
This commit is contained in:
commit
bbab2a9adc
|
@ -1,63 +1,66 @@
|
|||
name: Budibase CI
|
||||
|
||||
on:
|
||||
# Trigger the workflow on push or pull request,
|
||||
# but only for the master branch
|
||||
push:
|
||||
# Trigger the workflow on push or pull request,
|
||||
# but only for the master branch
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
pull_request:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
workflow_dispatch:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
BRANCH: ${{ github.event.pull_request.head.ref }}
|
||||
BASE_BRANCH: ${{ github.event.pull_request.base.ref}}
|
||||
PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 14.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14.x
|
||||
- run: yarn
|
||||
- run: yarn lint
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js 14.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14.x
|
||||
cache: "yarn"
|
||||
- run: yarn
|
||||
- run: yarn lint
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
- name: Use Node.js 14.x
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14.x
|
||||
- name: Install Pro
|
||||
run: yarn install:pro $BRANCH $BASE_BRANCH
|
||||
cache: "yarn"
|
||||
- run: yarn
|
||||
- run: yarn bootstrap
|
||||
- run: yarn build
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
- name: Use Node.js 14.x
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14.x
|
||||
- name: Install Pro
|
||||
run: yarn install:pro $BRANCH $BASE_BRANCH
|
||||
cache: "yarn"
|
||||
- run: yarn
|
||||
- run: yarn bootstrap
|
||||
- run: yarn build
|
||||
- run: yarn test
|
||||
- run: yarn build --scope=@budibase/types --scope=@budibase/shared-core --scope=@budibase/string-templates
|
||||
- run: yarn test --ignore=@budibase/pro
|
||||
- uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
||||
|
@ -67,32 +70,58 @@ jobs:
|
|||
test-pro:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
- name: Use Node.js 14.x
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14.x
|
||||
- name: Install Pro
|
||||
run: yarn install:pro $BRANCH $BASE_BRANCH
|
||||
cache: "yarn"
|
||||
- run: yarn
|
||||
- run: yarn bootstrap
|
||||
- run: yarn test:pro
|
||||
- run: yarn test --scope=@budibase/pro
|
||||
|
||||
integration-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
- name: Use Node.js 14.x
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14.x
|
||||
- name: Install Pro
|
||||
run: yarn install:pro $BRANCH $BASE_BRANCH
|
||||
- run: yarn && yarn bootstrap && yarn build
|
||||
- run: |
|
||||
cache: "yarn"
|
||||
- run: yarn
|
||||
- run: yarn build
|
||||
- name: Run tests
|
||||
run: |
|
||||
cd qa-core
|
||||
yarn setup
|
||||
yarn test:ci
|
||||
env:
|
||||
BB_ADMIN_USER_EMAIL: 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
|
||||
|
|
|
@ -2,20 +2,10 @@ name: Budibase Prerelease
|
|||
concurrency: release-prerelease
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
paths:
|
||||
- '.aws/**'
|
||||
- '.github/**'
|
||||
- 'charts/**'
|
||||
- 'packages/**'
|
||||
- 'scripts/**'
|
||||
- 'package.json'
|
||||
- 'yarn.lock'
|
||||
- 'package.json'
|
||||
- 'yarn.lock'
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- v*-alpha.*
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
# Posthog token used by ui at build time
|
||||
|
@ -30,24 +20,39 @@ jobs:
|
|||
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 }}
|
||||
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
|
||||
with:
|
||||
node-version: 14.x
|
||||
|
||||
- name: Install Pro
|
||||
run: yarn install:pro develop
|
||||
|
||||
- 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:sdk
|
||||
# - run: yarn test
|
||||
|
||||
- name: Publish budibase packages to NPM
|
||||
env:
|
||||
|
@ -56,6 +61,8 @@ jobs:
|
|||
# setup the username and email.
|
||||
git config --global user.name "Budibase Staging Release Bot"
|
||||
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
|
||||
yarn release:develop
|
||||
|
||||
|
@ -84,7 +91,7 @@ jobs:
|
|||
git config user.name "Budibase Helm Bot"
|
||||
git config user.email "<>"
|
||||
git reset --hard
|
||||
git pull
|
||||
git fetch
|
||||
mkdir sync
|
||||
echo "Packaging chart to sync dir"
|
||||
helm package charts/budibase --version 0.0.0-develop --app-version develop --destination sync
|
||||
|
|
|
@ -2,57 +2,60 @@ name: Budibase Release
|
|||
concurrency: release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- '.aws/**'
|
||||
- '.github/**'
|
||||
- 'charts/**'
|
||||
- 'packages/**'
|
||||
- 'scripts/**'
|
||||
- 'package.json'
|
||||
- 'yarn.lock'
|
||||
- 'package.json'
|
||||
- 'yarn.lock'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
versioning:
|
||||
type: choice
|
||||
description: "Versioning type: patch, minor, major"
|
||||
default: patch
|
||||
options:
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
required: true
|
||||
push:
|
||||
tags:
|
||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||
# Exclude all pre-releases
|
||||
- "!v*[0-9]+.[0-9]+.[0-9]+-*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tags:
|
||||
description: "Release tag"
|
||||
required: true
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
# Posthog token used by ui at build time
|
||||
POSTHOG_TOKEN: phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU
|
||||
INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
|
||||
jobs:
|
||||
release-images:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- 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
|
||||
// Change to "exit 1" when merged. Left to 0 to not fail all the pipelines and not to cause noise
|
||||
exit 0
|
||||
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14.x
|
||||
|
||||
- name: Install Pro
|
||||
run: yarn install:pro master
|
||||
|
||||
- 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 build
|
||||
- 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
|
||||
git config --global user.name "Budibase Release Bot"
|
||||
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
|
||||
yarn release
|
||||
|
||||
- name: 'Get Previous tag'
|
||||
- name: "Get Previous tag"
|
||||
id: previoustag
|
||||
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
||||
|
||||
|
@ -103,7 +108,7 @@ jobs:
|
|||
git config user.name "Budibase Helm Bot"
|
||||
git config user.email "<>"
|
||||
git reset --hard
|
||||
git pull
|
||||
git fetch
|
||||
mkdir sync
|
||||
echo "Packaging chart to sync dir"
|
||||
helm package charts/budibase --version 0.0.0-master --app-version v"$RELEASE_VERSION" --destination sync
|
||||
|
|
|
@ -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
|
|
@ -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 }}
|
|
@ -0,0 +1,3 @@
|
|||
[submodule "packages/pro"]
|
||||
path = packages/pro
|
||||
url = git@github.com:Budibase/budibase-pro.git
|
|
@ -0,0 +1,4 @@
|
|||
# .husky/post-checkout
|
||||
# ...
|
||||
|
||||
git config submodule.recurse true
|
|
@ -1,13 +1,17 @@
|
|||
## Dev Environment on Debian 11
|
||||
|
||||
### Install NVM & Node 14
|
||||
|
||||
NVM documentation: https://github.com/nvm-sh/nvm#installing-and-updating
|
||||
|
||||
Install NVM
|
||||
|
||||
```
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
|
||||
```
|
||||
|
||||
Install Node 14
|
||||
|
||||
```
|
||||
nvm install 14
|
||||
```
|
||||
|
@ -17,13 +21,16 @@ nvm install 14
|
|||
```
|
||||
npm install -g yarn jest lerna
|
||||
```
|
||||
|
||||
### Install Docker and Docker Compose
|
||||
|
||||
```
|
||||
apt install docker.io
|
||||
pip3 install docker-compose
|
||||
```
|
||||
|
||||
### Clone the repo
|
||||
|
||||
```
|
||||
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
|
||||
yarn setup
|
||||
```
|
||||
|
||||
The yarn setup command runs several build steps i.e.
|
||||
|
||||
```
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
### 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.
|
||||
To fix this, apply the following tweaks.
|
||||
|
||||
|
|
|
@ -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
|
||||
through brew.
|
||||
|
||||
|
||||
### Install Node
|
||||
|
||||
Budibase requires a recent version of node 14:
|
||||
|
||||
```
|
||||
brew install node npm
|
||||
node -v
|
||||
|
@ -22,12 +22,15 @@ node -v
|
|||
```
|
||||
npm install -g yarn jest lerna
|
||||
```
|
||||
|
||||
### Install Docker and Docker Compose
|
||||
|
||||
```
|
||||
brew install docker docker-compose
|
||||
```
|
||||
|
||||
### Clone the repo
|
||||
|
||||
```
|
||||
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
|
||||
yarn setup
|
||||
```
|
||||
|
||||
The yarn setup command runs several build steps i.e.
|
||||
|
||||
```
|
||||
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.
|
||||
|
||||
The dev version will be available on port 10000 i.e.
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
## Dev Environment on Windows 10/11 (WSL2)
|
||||
|
||||
|
||||
### Install WSL with Ubuntu LTS
|
||||
|
||||
Enable WSL 2 on Windows 10/11 for docker support.
|
||||
|
||||
```
|
||||
wsl --set-default-version 2
|
||||
```
|
||||
|
||||
Install Ubuntu LTS.
|
||||
|
||||
```
|
||||
wsl --install Ubuntu
|
||||
```
|
||||
|
@ -16,6 +18,7 @@ Or follow the instruction here:
|
|||
https://learn.microsoft.com/en-us/windows/wsl/install
|
||||
|
||||
### Install Docker in windows
|
||||
|
||||
Download the installer from docker and install it.
|
||||
|
||||
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.
|
||||
|
||||
### Install NVM & Node 14
|
||||
|
||||
NVM documentation: https://github.com/nvm-sh/nvm#installing-and-updating
|
||||
|
||||
Install NVM
|
||||
|
||||
```
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
|
||||
```
|
||||
|
||||
Install Node 14
|
||||
|
||||
```
|
||||
nvm install 14
|
||||
```
|
||||
|
||||
|
||||
### Install npm requirements
|
||||
|
||||
```
|
||||
|
@ -43,6 +49,7 @@ npm install -g yarn jest lerna
|
|||
```
|
||||
|
||||
### Clone the repo
|
||||
|
||||
```
|
||||
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
|
||||
yarn setup
|
||||
```
|
||||
|
||||
The yarn setup command runs several build steps i.e.
|
||||
|
||||
```
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
### 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.
|
||||
|
||||
https://code.visualstudio.com/docs/remote/wsl
|
||||
|
|
|
@ -5,8 +5,11 @@ ENV COUCHDB_PASSWORD admin
|
|||
EXPOSE 5984
|
||||
|
||||
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' && \
|
||||
apt-get update && apt-get install -y --no-install-recommends openjdk-8-jre && \
|
||||
wget -qO - https://adoptopenjdk.jfrog.io/adoptopenjdk/api/gpg/key/public | apt-key add - && \
|
||||
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/
|
||||
|
||||
# setup clouseau
|
||||
|
|
|
@ -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_object "object-src 'none'";
|
||||
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_frame "frame-src 'self' https:";
|
||||
set $csp_img "img-src http: https: data: blob:";
|
||||
|
@ -82,6 +82,12 @@ http {
|
|||
set $couchdb ${COUCHDB_UPSTREAM_URL};
|
||||
set $watchtower ${WATCHTOWER_UPSTREAM_URL};
|
||||
|
||||
location /health {
|
||||
access_log off;
|
||||
add_header 'Content-Type' 'application/json';
|
||||
return 200 '{ "status": "OK" }';
|
||||
}
|
||||
|
||||
location /app {
|
||||
proxy_pass $apps;
|
||||
}
|
||||
|
@ -222,9 +228,9 @@ http {
|
|||
rewrite ^/files/signed/(.*)$ /$1 break;
|
||||
}
|
||||
|
||||
client_header_timeout 60;
|
||||
client_body_timeout 60;
|
||||
keepalive_timeout 60;
|
||||
client_header_timeout 120;
|
||||
client_body_timeout 120;
|
||||
keepalive_timeout 120;
|
||||
|
||||
# gzip
|
||||
gzip on;
|
||||
|
|
|
@ -22,7 +22,7 @@ FROM budibase/couchdb
|
|||
ARG TARGETARCH
|
||||
ENV TARGETARCH $TARGETARCH
|
||||
#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
|
||||
ENV TARGETBUILD $TARGETBUILD
|
||||
|
||||
|
@ -32,7 +32,7 @@ COPY --from=build /worker /worker
|
|||
# install base dependencies
|
||||
RUN apt-get update && \
|
||||
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
|
||||
|
||||
# install other dependencies, nodejs, oracle requirements, jdk8, redis, nginx
|
||||
|
|
18
lerna.json
18
lerna.json
|
@ -1,8 +1,22 @@
|
|||
{
|
||||
"version": "2.5.6-alpha.30",
|
||||
"version": "2.6.19-alpha.4",
|
||||
"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,
|
||||
"packages": ["packages/*"],
|
||||
"command": {
|
||||
"publish": {
|
||||
"ignoreChanges": [
|
||||
|
|
36
package.json
36
package.json
|
@ -8,7 +8,7 @@
|
|||
"eslint": "^7.28.0",
|
||||
"eslint-plugin-cypress": "^2.11.3",
|
||||
"eslint-plugin-svelte3": "^3.2.0",
|
||||
"husky": "^7.0.1",
|
||||
"husky": "^8.0.3",
|
||||
"js-yaml": "^4.1.0",
|
||||
"kill-port": "^1.6.1",
|
||||
"lerna": "^6.6.1",
|
||||
|
@ -17,22 +17,22 @@
|
|||
"prettier-plugin-svelte": "^2.3.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"rollup-plugin-replace": "^2.2.0",
|
||||
"semver": "^7.5.0",
|
||||
"svelte": "^3.38.2",
|
||||
"typescript": "4.7.3"
|
||||
},
|
||||
"scripts": {
|
||||
"setup": "node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev",
|
||||
"bootstrap": "lerna link && ./scripts/link-dependencies.sh",
|
||||
"preinstall": "node scripts/syncProPackage.js",
|
||||
"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: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:build": "./scripts/scopeBackend.sh 'lerna run --stream build'",
|
||||
"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",
|
||||
"release": "lerna publish ${RELEASE_VERSION_TYPE:-patch} --yes --force-publish && yarn release:pro",
|
||||
"release:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop --exact && yarn release:pro:develop",
|
||||
"release:pro": "bash scripts/pro/release.sh",
|
||||
"release:pro:develop": "bash scripts/pro/release.sh develop",
|
||||
"release": "lerna publish ${RELEASE_VERSION_TYPE:-patch} --yes --force-publish --no-git-tag-version --no-push --no-git-reset",
|
||||
"release:develop": "lerna publish from-package --yes --force-publish --dist-tag develop --exact --no-git-tag-version --no-push --no-git-reset",
|
||||
"restore": "yarn run clean && yarn run bootstrap && yarn run build",
|
||||
"nuke": "yarn run nuke:packages && yarn run nuke:docker",
|
||||
"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: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:pro": "bash scripts/pro/test.sh",
|
||||
"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": "yarn run lint:eslint && yarn run lint:prettier",
|
||||
|
@ -82,12 +81,25 @@
|
|||
"mode:account": "yarn mode:cloud && yarn env:account:enable",
|
||||
"security:audit": "node scripts/audit.js",
|
||||
"postinstall": "husky install",
|
||||
"install:pro": "bash scripts/pro/install.sh",
|
||||
"dep:clean": "yarn clean && yarn bootstrap"
|
||||
"dep:clean": "yarn clean -y && yarn bootstrap",
|
||||
"submodules:load": "git submodule init && git submodule update && yarn && yarn bootstrap",
|
||||
"submodules:unload": "git submodule deinit --all && yarn && yarn bootstrap"
|
||||
},
|
||||
"workspaces": {
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/backend-core",
|
||||
"version": "2.5.6-alpha.30",
|
||||
"version": "0.0.1",
|
||||
"description": "Budibase backend core libraries used in server and worker",
|
||||
"main": "dist/src/index.js",
|
||||
"types": "dist/src/index.d.ts",
|
||||
|
@ -15,8 +15,6 @@
|
|||
"prebuild": "rimraf dist/",
|
||||
"prepack": "cp package.json dist",
|
||||
"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",
|
||||
"test": "bash scripts/test.sh",
|
||||
"test:watch": "jest --watchAll"
|
||||
|
@ -24,7 +22,7 @@
|
|||
"dependencies": {
|
||||
"@budibase/nano": "10.1.2",
|
||||
"@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",
|
||||
"@techpass/passport-openidconnect": "0.3.2",
|
||||
"aws-cloudfront-sign": "2.2.0",
|
||||
|
|
|
@ -47,7 +47,7 @@ async function put(
|
|||
type: LockType.TRY_ONCE,
|
||||
name: LockName.PERSIST_WRITETHROUGH,
|
||||
resource: key,
|
||||
ttl: 1000,
|
||||
ttl: 15000,
|
||||
},
|
||||
async () => {
|
||||
const writeDb = async (toWrite: any) => {
|
||||
|
@ -71,6 +71,7 @@ async function put(
|
|||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (!lockResponse.executed) {
|
||||
logWarn(`Ignoring redlock conflict in write-through cache`)
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ export enum ViewName {
|
|||
AUTOMATION_LOGS = "automation_logs",
|
||||
ACCOUNT_BY_EMAIL = "account_by_email",
|
||||
PLATFORM_USERS_LOWERCASE = "platform_users_lowercase",
|
||||
USER_BY_GROUP = "by_group_user",
|
||||
USER_BY_GROUP = "user_by_group",
|
||||
APP_BACKUP_BY_TRIGGER = "by_trigger",
|
||||
}
|
||||
|
||||
|
|
|
@ -104,6 +104,22 @@ async function newContext(updates: ContextMap, task: any) {
|
|||
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> {
|
||||
const tenantId = getTenantIDFromAppID(appId)
|
||||
return newContext(
|
||||
|
@ -187,6 +203,11 @@ export function getTenantId(): string {
|
|||
return tenantId
|
||||
}
|
||||
|
||||
export function getAutomationId(): string | undefined {
|
||||
const context = Context.get()
|
||||
return context?.automationId
|
||||
}
|
||||
|
||||
export function getAppId(): string | undefined {
|
||||
const context = Context.get()
|
||||
const foundId = context?.appId
|
||||
|
|
|
@ -7,4 +7,5 @@ export type ContextMap = {
|
|||
identity?: IdentityContext
|
||||
environmentVariables?: Record<string, string>
|
||||
isScim?: boolean
|
||||
automationId?: string
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
isDocument,
|
||||
} from "@budibase/types"
|
||||
import { getCouchInfo } from "./connections"
|
||||
import { directCouchCall } from "./utils"
|
||||
import { directCouchUrlCall } from "./utils"
|
||||
import { getPouchDB } from "./pouchDB"
|
||||
import { WriteStream, ReadStream } from "fs"
|
||||
import { newid } from "../../docIds/newid"
|
||||
|
@ -46,6 +46,8 @@ export class DatabaseImpl implements Database {
|
|||
private readonly instanceNano?: Nano.ServerScope
|
||||
private readonly pouchOpts: DatabaseOpts
|
||||
|
||||
private readonly couchInfo = getCouchInfo()
|
||||
|
||||
constructor(dbName?: string, opts?: DatabaseOpts, connection?: string) {
|
||||
if (dbName == null) {
|
||||
throw new Error("Database name cannot be undefined.")
|
||||
|
@ -53,8 +55,8 @@ export class DatabaseImpl implements Database {
|
|||
this.name = dbName
|
||||
this.pouchOpts = opts || {}
|
||||
if (connection) {
|
||||
const couchInfo = getCouchInfo(connection)
|
||||
this.instanceNano = buildNano(couchInfo)
|
||||
this.couchInfo = getCouchInfo(connection)
|
||||
this.instanceNano = buildNano(this.couchInfo)
|
||||
}
|
||||
if (!DatabaseImpl.nano) {
|
||||
DatabaseImpl.init()
|
||||
|
@ -67,7 +69,11 @@ export class DatabaseImpl implements Database {
|
|||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -4,21 +4,21 @@ export const getCouchInfo = (connection?: string) => {
|
|||
const urlInfo = getUrlInfo(connection)
|
||||
let username
|
||||
let password
|
||||
if (env.COUCH_DB_USERNAME) {
|
||||
// set from env
|
||||
username = env.COUCH_DB_USERNAME
|
||||
} else if (urlInfo.auth.username) {
|
||||
if (urlInfo.auth?.username) {
|
||||
// set from url
|
||||
username = urlInfo.auth.username
|
||||
} else if (env.COUCH_DB_USERNAME) {
|
||||
// set from env
|
||||
username = env.COUCH_DB_USERNAME
|
||||
} else if (!env.isTest()) {
|
||||
throw new Error("CouchDB username not set")
|
||||
}
|
||||
if (env.COUCH_DB_PASSWORD) {
|
||||
// set from env
|
||||
password = env.COUCH_DB_PASSWORD
|
||||
} else if (urlInfo.auth.password) {
|
||||
if (urlInfo.auth?.password) {
|
||||
// set from url
|
||||
password = urlInfo.auth.password
|
||||
} else if (env.COUCH_DB_PASSWORD) {
|
||||
// set from env
|
||||
password = env.COUCH_DB_PASSWORD
|
||||
} else if (!env.isTest()) {
|
||||
throw new Error("CouchDB password not set")
|
||||
}
|
||||
|
|
|
@ -9,6 +9,20 @@ export async function directCouchCall(
|
|||
) {
|
||||
let { url, cookie } = getCouchInfo()
|
||||
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 = {
|
||||
method: method,
|
||||
headers: {
|
||||
|
@ -19,7 +33,7 @@ export async function directCouchCall(
|
|||
params.body = JSON.stringify(body)
|
||||
params.headers["Content-Type"] = "application/json"
|
||||
}
|
||||
return await fetch(checkSlashesInUrl(encodeURI(couchUrl)), params)
|
||||
return await fetch(checkSlashesInUrl(encodeURI(url)), params)
|
||||
}
|
||||
|
||||
export async function directCouchQuery(
|
||||
|
|
|
@ -434,7 +434,7 @@ export class QueryBuilder<T> {
|
|||
})
|
||||
}
|
||||
if (this.#query.empty) {
|
||||
build(this.#query.empty, (key: string) => `!${key}:["" TO *]`)
|
||||
build(this.#query.empty, (key: string) => `(*:* -${key}:["" TO *])`)
|
||||
}
|
||||
if (this.#query.notEmpty) {
|
||||
build(this.#query.notEmpty, (key: string) => `${key}:["" TO *]`)
|
||||
|
|
|
@ -69,10 +69,10 @@ function findVersion() {
|
|||
try {
|
||||
const packageJsonFile = findFileInAncestors("package.json", process.cwd())
|
||||
const content = readFileSync(packageJsonFile!, "utf-8")
|
||||
const version = JSON.parse(content).version
|
||||
return version
|
||||
return JSON.parse(content).version
|
||||
} 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,
|
||||
SALT_ROUNDS: process.env.SALT_ROUNDS,
|
||||
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,
|
||||
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
|
||||
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
|
||||
|
@ -154,6 +155,7 @@ const environment = {
|
|||
? process.env.ENABLE_SSO_MAINTENANCE_MODE
|
||||
: false,
|
||||
VERSION: findVersion(),
|
||||
DISABLE_PINO_LOGGER: process.env.DISABLE_PINO_LOGGER,
|
||||
_set(key: any, value: any) {
|
||||
process.env[key] = value
|
||||
// @ts-ignore
|
||||
|
|
|
@ -3,7 +3,6 @@ import {
|
|||
Event,
|
||||
LicenseActivatedEvent,
|
||||
LicensePlanChangedEvent,
|
||||
LicenseTierChangedEvent,
|
||||
PlanType,
|
||||
Account,
|
||||
LicensePortalOpenedEvent,
|
||||
|
@ -11,22 +10,23 @@ import {
|
|||
LicenseCheckoutOpenedEvent,
|
||||
LicensePaymentFailedEvent,
|
||||
LicensePaymentRecoveredEvent,
|
||||
PriceDuration,
|
||||
} from "@budibase/types"
|
||||
|
||||
async function tierChanged(account: Account, from: number, to: number) {
|
||||
const properties: LicenseTierChangedEvent = {
|
||||
accountId: account.accountId,
|
||||
to,
|
||||
from,
|
||||
async function planChanged(
|
||||
account: Account,
|
||||
opts: {
|
||||
from: PlanType
|
||||
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 = {
|
||||
accountId: account.accountId,
|
||||
to,
|
||||
from,
|
||||
...opts,
|
||||
}
|
||||
await publishEvent(Event.LICENSE_PLAN_CHANGED, properties)
|
||||
}
|
||||
|
@ -74,7 +74,6 @@ async function paymentRecovered(account: Account) {
|
|||
}
|
||||
|
||||
export default {
|
||||
tierChanged,
|
||||
planChanged,
|
||||
activated,
|
||||
checkoutOpened,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export * as correlation from "./correlation/correlation"
|
||||
export { logger, disableLogger } from "./pino/logger"
|
||||
export { logger } from "./pino/logger"
|
||||
export * from "./alerts"
|
||||
|
||||
// turn off or on context logging i.e. tenantId, appId etc
|
||||
|
|
|
@ -5,184 +5,206 @@ import * as correlation from "../correlation"
|
|||
import { IdentityType } from "@budibase/types"
|
||||
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
|
||||
|
||||
const pinoOptions: LoggerOptions = {
|
||||
level: env.LOG_LEVEL,
|
||||
formatters: {
|
||||
level: label => {
|
||||
return { level: label.toUpperCase() }
|
||||
},
|
||||
bindings: () => {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
timestamp: () => `,"timestamp":"${new Date(Date.now()).toISOString()}"`,
|
||||
}
|
||||
|
||||
if (env.isDev()) {
|
||||
pinoOptions.transport = {
|
||||
target: "pino-pretty",
|
||||
options: {
|
||||
singleLine: true,
|
||||
let pinoInstance: pino.Logger | undefined
|
||||
if (!env.DISABLE_PINO_LOGGER) {
|
||||
const pinoOptions: LoggerOptions = {
|
||||
level: env.LOG_LEVEL,
|
||||
formatters: {
|
||||
level: label => {
|
||||
return { level: label.toUpperCase() }
|
||||
},
|
||||
bindings: () => {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
timestamp: () => `,"timestamp":"${new Date(Date.now()).toISOString()}"`,
|
||||
}
|
||||
}
|
||||
|
||||
export const logger = 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
|
||||
|
||||
interface MergingObject {
|
||||
objects?: any[]
|
||||
tenantId?: string
|
||||
appId?: string
|
||||
identityId?: string
|
||||
identityType?: IdentityType
|
||||
correlationId?: string
|
||||
err?: Error
|
||||
}
|
||||
|
||||
function isPlainObject(obj: any) {
|
||||
return typeof obj === "object" && obj !== null && !(obj instanceof Error)
|
||||
}
|
||||
|
||||
function isError(obj: any) {
|
||||
return obj instanceof Error
|
||||
}
|
||||
|
||||
function isMessage(obj: any) {
|
||||
return typeof obj === "string"
|
||||
}
|
||||
|
||||
/**
|
||||
* Backwards compatibility between console logging statements
|
||||
* and pino logging requirements.
|
||||
*/
|
||||
function getLogParams(args: any[]): [MergingObject, string] {
|
||||
let error = undefined
|
||||
let objects: any[] = []
|
||||
let message = ""
|
||||
|
||||
args.forEach(arg => {
|
||||
if (isMessage(arg)) {
|
||||
message = `${message} ${arg}`.trimStart()
|
||||
}
|
||||
if (isPlainObject(arg)) {
|
||||
objects.push(arg)
|
||||
}
|
||||
if (isError(arg)) {
|
||||
error = arg
|
||||
}
|
||||
})
|
||||
|
||||
const identity = getIdentity()
|
||||
|
||||
let contextObject = {}
|
||||
|
||||
if (LOG_CONTEXT) {
|
||||
contextObject = {
|
||||
tenantId: getTenantId(),
|
||||
appId: getAppId(),
|
||||
identityId: identity?._id,
|
||||
identityType: identity?.type,
|
||||
correlationId: correlation.getId(),
|
||||
if (env.isDev()) {
|
||||
pinoOptions.transport = {
|
||||
target: "pino-pretty",
|
||||
options: {
|
||||
singleLine: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const mergingObject = {
|
||||
objects: objects.length ? objects : undefined,
|
||||
err: error,
|
||||
...contextObject,
|
||||
pinoInstance = pino(pinoOptions)
|
||||
|
||||
// CONSOLE OVERRIDES
|
||||
|
||||
interface MergingObject {
|
||||
objects?: any[]
|
||||
tenantId?: string
|
||||
appId?: string
|
||||
automationId?: string
|
||||
identityId?: string
|
||||
identityType?: IdentityType
|
||||
correlationId?: string
|
||||
err?: Error
|
||||
}
|
||||
|
||||
return [mergingObject, message]
|
||||
}
|
||||
|
||||
console.log = (...arg: any[]) => {
|
||||
const [obj, msg] = getLogParams(arg)
|
||||
logger.info(obj, msg)
|
||||
}
|
||||
console.info = (...arg: any[]) => {
|
||||
const [obj, msg] = getLogParams(arg)
|
||||
logger.info(obj, msg)
|
||||
}
|
||||
console.warn = (...arg: any[]) => {
|
||||
const [obj, msg] = getLogParams(arg)
|
||||
logger.warn(obj, msg)
|
||||
}
|
||||
console.error = (...arg: any[]) => {
|
||||
const [obj, msg] = getLogParams(arg)
|
||||
logger.error(obj, msg)
|
||||
}
|
||||
|
||||
/**
|
||||
* custom trace impl - this resembles the node trace behaviour rather
|
||||
* than traditional trace logging
|
||||
* @param arg
|
||||
*/
|
||||
console.trace = (...arg: any[]) => {
|
||||
const [obj, msg] = getLogParams(arg)
|
||||
if (!obj.err) {
|
||||
// to get stack trace
|
||||
obj.err = new Error()
|
||||
function isPlainObject(obj: any) {
|
||||
return typeof obj === "object" && obj !== null && !(obj instanceof Error)
|
||||
}
|
||||
logger.trace(obj, msg)
|
||||
}
|
||||
|
||||
console.debug = (...arg: any) => {
|
||||
const [obj, msg] = getLogParams(arg)
|
||||
logger.debug(obj, msg)
|
||||
}
|
||||
|
||||
// CONTEXT
|
||||
|
||||
const getTenantId = () => {
|
||||
let tenantId
|
||||
try {
|
||||
tenantId = context.getTenantId()
|
||||
} catch (e: any) {
|
||||
// do nothing
|
||||
function isError(obj: any) {
|
||||
return obj instanceof Error
|
||||
}
|
||||
|
||||
function isMessage(obj: any) {
|
||||
return typeof obj === "string"
|
||||
}
|
||||
|
||||
/**
|
||||
* Backwards compatibility between console logging statements
|
||||
* and pino logging requirements.
|
||||
*/
|
||||
function getLogParams(args: any[]): [MergingObject, string] {
|
||||
let error = undefined
|
||||
let objects: any[] = []
|
||||
let message = ""
|
||||
|
||||
args.forEach(arg => {
|
||||
if (isMessage(arg)) {
|
||||
message = `${message} ${arg}`.trimStart()
|
||||
}
|
||||
if (isPlainObject(arg)) {
|
||||
objects.push(arg)
|
||||
}
|
||||
if (isError(arg)) {
|
||||
error = arg
|
||||
}
|
||||
})
|
||||
|
||||
const identity = getIdentity()
|
||||
|
||||
let contextObject = {}
|
||||
|
||||
if (LOG_CONTEXT) {
|
||||
contextObject = {
|
||||
tenantId: getTenantId(),
|
||||
appId: getAppId(),
|
||||
automationId: getAutomationId(),
|
||||
identityId: identity?._id,
|
||||
identityType: identity?.type,
|
||||
correlationId: correlation.getId(),
|
||||
}
|
||||
}
|
||||
|
||||
const mergingObject: any = {
|
||||
err: error,
|
||||
...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]
|
||||
}
|
||||
|
||||
console.log = (...arg: any[]) => {
|
||||
const [obj, msg] = getLogParams(arg)
|
||||
pinoInstance?.info(obj, msg)
|
||||
}
|
||||
console.info = (...arg: any[]) => {
|
||||
const [obj, msg] = getLogParams(arg)
|
||||
pinoInstance?.info(obj, msg)
|
||||
}
|
||||
console.warn = (...arg: any[]) => {
|
||||
const [obj, msg] = getLogParams(arg)
|
||||
pinoInstance?.warn(obj, msg)
|
||||
}
|
||||
console.error = (...arg: any[]) => {
|
||||
const [obj, msg] = getLogParams(arg)
|
||||
pinoInstance?.error(obj, msg)
|
||||
}
|
||||
|
||||
/**
|
||||
* custom trace impl - this resembles the node trace behaviour rather
|
||||
* than traditional trace logging
|
||||
* @param arg
|
||||
*/
|
||||
console.trace = (...arg: any[]) => {
|
||||
const [obj, msg] = getLogParams(arg)
|
||||
if (!obj.err) {
|
||||
// to get stack trace
|
||||
obj.err = new Error()
|
||||
}
|
||||
pinoInstance?.trace(obj, msg)
|
||||
}
|
||||
|
||||
console.debug = (...arg: any) => {
|
||||
const [obj, msg] = getLogParams(arg)
|
||||
pinoInstance?.debug(obj, msg)
|
||||
}
|
||||
|
||||
// CONTEXT
|
||||
|
||||
const getTenantId = () => {
|
||||
let tenantId
|
||||
try {
|
||||
tenantId = context.getTenantId()
|
||||
} catch (e: any) {
|
||||
// do nothing
|
||||
}
|
||||
return tenantId
|
||||
}
|
||||
|
||||
const getAppId = () => {
|
||||
let appId
|
||||
try {
|
||||
appId = context.getAppId()
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
return appId
|
||||
}
|
||||
|
||||
const getAutomationId = () => {
|
||||
let appId
|
||||
try {
|
||||
appId = context.getAutomationId()
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
return appId
|
||||
}
|
||||
|
||||
const getIdentity = () => {
|
||||
let identity
|
||||
try {
|
||||
identity = context.getIdentity()
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
return identity
|
||||
}
|
||||
return tenantId
|
||||
}
|
||||
|
||||
const getAppId = () => {
|
||||
let appId
|
||||
try {
|
||||
appId = context.getAppId()
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
return appId
|
||||
}
|
||||
|
||||
const getIdentity = () => {
|
||||
let identity
|
||||
try {
|
||||
identity = context.getIdentity()
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
return identity
|
||||
}
|
||||
export const logger = pinoInstance
|
||||
|
|
|
@ -128,6 +128,7 @@ class InMemoryQueue {
|
|||
|
||||
on() {
|
||||
// do nothing
|
||||
return this
|
||||
}
|
||||
|
||||
async waitForCompletion() {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Job, JobId, Queue } from "bull"
|
||||
import { JobQueue } from "./constants"
|
||||
import * as context from "../context"
|
||||
|
||||
export type StalledFn = (job: Job) => Promise<void>
|
||||
|
||||
|
@ -31,71 +32,164 @@ function handleStalled(queue: Queue, removeStalledCb?: StalledFn) {
|
|||
})
|
||||
}
|
||||
|
||||
function logging(queue: Queue, jobQueue: JobQueue) {
|
||||
let eventType: string
|
||||
switch (jobQueue) {
|
||||
case JobQueue.AUTOMATION:
|
||||
eventType = "automation-event"
|
||||
break
|
||||
case JobQueue.APP_BACKUP:
|
||||
eventType = "app-backup-event"
|
||||
break
|
||||
function getLogParams(
|
||||
eventType: QueueEventType,
|
||||
event: BullEvent,
|
||||
opts: {
|
||||
job?: Job
|
||||
jobId?: JobId
|
||||
error?: Error
|
||||
} = {},
|
||||
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,
|
||||
}
|
||||
|
||||
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
|
||||
.on(BullEvent.STALLED, async (job: Job) => {
|
||||
// A job has been marked as stalled. This is useful for debugging job
|
||||
// workers that crash or pause the event loop.
|
||||
await doInJobContext(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("error", (error: any) => {
|
||||
// An error occurred.
|
||||
console.error(`${eventType}=error error=${JSON.stringify(error)}`)
|
||||
})
|
||||
.on("waiting", (jobId: JobId) => {
|
||||
.on(BullEvent.WAITING, (jobId: JobId) => {
|
||||
// A Job is waiting to be processed as soon as a worker is idling.
|
||||
console.log(`${eventType}=waiting jobId=${jobId}`)
|
||||
console.info(...getLogParams(eventType, BullEvent.WAITING, { jobId }))
|
||||
})
|
||||
.on("active", (job: Job, jobPromise: any) => {
|
||||
.on(BullEvent.ACTIVE, async (job: Job, jobPromise: any) => {
|
||||
// A job has started. You can use `jobPromise.cancel()`` to abort it.
|
||||
console.log(`${eventType}=active jobId=${job.id}`)
|
||||
await doInJobContext(job, () => {
|
||||
console.info(...getLogParams(eventType, BullEvent.ACTIVE, { job }))
|
||||
})
|
||||
})
|
||||
.on("stalled", (job: Job) => {
|
||||
// A job has been marked as stalled. This is useful for debugging job
|
||||
// workers that crash or pause the event loop.
|
||||
console.error(
|
||||
`${eventType}=stalled jobId=${job.id} job=${JSON.stringify(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`.
|
||||
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`!
|
||||
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.
|
||||
console.log(`${eventType}=paused`)
|
||||
console.info(...getLogParams(eventType, BullEvent.PAUSED))
|
||||
})
|
||||
.on("resumed", (job: Job) => {
|
||||
.on(BullEvent.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
|
||||
// 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)
|
||||
console.log(`${eventType}=drained`)
|
||||
console.info(...getLogParams(eventType, BullEvent.DRAINED))
|
||||
})
|
||||
.on("removed", (job: Job) => {
|
||||
.on(BullEvent.REMOVED, (job: Job) => {
|
||||
// A job successfully removed.
|
||||
console.log(`${eventType}=removed jobId=${job.id}`)
|
||||
console.info(...getLogParams(eventType, BullEvent.REMOVED, { job }))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import * as timers from "../timers"
|
|||
|
||||
const RETRY_PERIOD_MS = 2000
|
||||
const STARTUP_TIMEOUT_MS = 5000
|
||||
const CLUSTERED = false
|
||||
const CLUSTERED = env.REDIS_CLUSTERED
|
||||
const DEFAULT_SELECT_DB = SelectableDatabase.DEFAULT
|
||||
|
||||
// for testing just generate the client once
|
||||
|
@ -81,7 +81,7 @@ function init(selectDb = DEFAULT_SELECT_DB) {
|
|||
if (client) {
|
||||
client.disconnect()
|
||||
}
|
||||
const { redisProtocolUrl, opts, host, port } = getRedisOptions(CLUSTERED)
|
||||
const { redisProtocolUrl, opts, host, port } = getRedisOptions()
|
||||
|
||||
if (CLUSTERED) {
|
||||
client = new Redis.Cluster([{ host, port }], opts)
|
||||
|
|
|
@ -85,7 +85,7 @@ export const doWithLock = async <T>(
|
|||
opts: LockOptions,
|
||||
task: () => Promise<T>
|
||||
): Promise<RedlockExecution<T>> => {
|
||||
const redlock = await getClient(opts.type)
|
||||
const redlock = await getClient(opts.type, opts.customOptions)
|
||||
let lock
|
||||
try {
|
||||
// determine lock name
|
||||
|
|
|
@ -57,7 +57,7 @@ export enum SelectableDatabase {
|
|||
UNUSED_14 = 15,
|
||||
}
|
||||
|
||||
export function getRedisOptions(clustered = false) {
|
||||
export function getRedisOptions() {
|
||||
let password = env.REDIS_PASSWORD
|
||||
let url: string[] | string = env.REDIS_URL.split("//")
|
||||
// get rid of the protocol
|
||||
|
@ -83,7 +83,7 @@ export function getRedisOptions(clustered = false) {
|
|||
const opts: any = {
|
||||
connectTimeout: CONNECT_TIMEOUT_MS,
|
||||
}
|
||||
if (clustered) {
|
||||
if (env.REDIS_CLUSTERED) {
|
||||
opts.redisOptions = {}
|
||||
opts.redisOptions.tls = {}
|
||||
opts.redisOptions.password = password
|
||||
|
|
|
@ -5,6 +5,7 @@ import * as db from "../../db"
|
|||
import { Header } from "../../constants"
|
||||
import { newid } from "../../utils"
|
||||
import env from "../../environment"
|
||||
import { BBContext } from "@budibase/types"
|
||||
|
||||
describe("utils", () => {
|
||||
const config = new DBTestConfiguration()
|
||||
|
@ -106,4 +107,85 @@ describe("utils", () => {
|
|||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
import { getAllApps, queryGlobalView } from "../db"
|
||||
import {
|
||||
Header,
|
||||
MAX_VALID_DATE,
|
||||
DocumentType,
|
||||
SEPARATOR,
|
||||
ViewName,
|
||||
} from "../constants"
|
||||
import { getAllApps } from "../db"
|
||||
import { Header, MAX_VALID_DATE, DocumentType, SEPARATOR } from "../constants"
|
||||
import env from "../environment"
|
||||
import * as tenancy from "../tenancy"
|
||||
import * as context from "../context"
|
||||
|
@ -23,7 +17,9 @@ const APP_PREFIX = DocumentType.APP + SEPARATOR
|
|||
const PROD_APP_PREFIX = "/app/"
|
||||
|
||||
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) {
|
||||
return possibleAppId && possibleAppId.startsWith(APP_PREFIX)
|
||||
|
@ -69,6 +65,18 @@ export function isServingApp(ctx: Ctx) {
|
|||
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
|
||||
* @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
|
||||
// referer header is present from a builder redirect
|
||||
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)
|
||||
appId = confirmAppId(refererId)
|
||||
}
|
||||
|
|
|
@ -123,7 +123,6 @@ beforeAll(async () => {
|
|||
jest.spyOn(events.plugin, "imported")
|
||||
jest.spyOn(events.plugin, "deleted")
|
||||
|
||||
jest.spyOn(events.license, "tierChanged")
|
||||
jest.spyOn(events.license, "planChanged")
|
||||
jest.spyOn(events.license, "activated")
|
||||
jest.spyOn(events.license, "checkoutOpened")
|
||||
|
|
|
@ -7,16 +7,29 @@ import {
|
|||
PlanType,
|
||||
PriceDuration,
|
||||
PurchasedPlan,
|
||||
PurchasedPrice,
|
||||
Quotas,
|
||||
Subscription,
|
||||
} 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 => {
|
||||
return {
|
||||
type,
|
||||
usesInvoicing: false,
|
||||
minUsers: 1,
|
||||
model: PlanModel.PER_USER,
|
||||
price: type !== PlanType.FREE ? price() : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,11 +7,6 @@
|
|||
"@budibase/types": ["../types/src"]
|
||||
}
|
||||
},
|
||||
"references": [
|
||||
{ "path": "../types" }
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
]
|
||||
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/bbui",
|
||||
"description": "A UI solution used in the different Budibase projects.",
|
||||
"version": "2.5.6-alpha.30",
|
||||
"version": "0.0.1",
|
||||
"license": "MPL-2.0",
|
||||
"svelte": "src/index.js",
|
||||
"module": "dist/bbui.es.js",
|
||||
|
@ -38,8 +38,8 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"@adobe/spectrum-css-workflow-icons": "1.2.1",
|
||||
"@budibase/shared-core": "2.5.6-alpha.30",
|
||||
"@budibase/string-templates": "2.5.6-alpha.30",
|
||||
"@budibase/shared-core": "0.0.1",
|
||||
"@budibase/string-templates": "0.0.1",
|
||||
"@spectrum-css/accordion": "3.0.24",
|
||||
"@spectrum-css/actionbutton": "1.0.1",
|
||||
"@spectrum-css/actiongroup": "1.0.1",
|
||||
|
@ -84,7 +84,7 @@
|
|||
"@spectrum-css/vars": "3.0.1",
|
||||
"dayjs": "^1.10.4",
|
||||
"easymde": "^2.16.1",
|
||||
"svelte-flatpickr": "^3.3.2",
|
||||
"svelte-flatpickr": "3.2.3",
|
||||
"svelte-portal": "^1.0.0"
|
||||
},
|
||||
"resolutions": {
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
const ignoredClasses = [".flatpickr-calendar", ".spectrum-Popover"]
|
||||
const ignoredClasses = [
|
||||
".flatpickr-calendar",
|
||||
".spectrum-Popover",
|
||||
".download-js-link",
|
||||
]
|
||||
let clickHandlers = []
|
||||
|
||||
/**
|
||||
|
@ -22,8 +26,8 @@ const handleClick = event => {
|
|||
}
|
||||
|
||||
// Ignore clicks for modals, unless the handler is registered from a modal
|
||||
const sourceInModal = handler.anchor.closest(".spectrum-Modal") != null
|
||||
const clickInModal = event.target.closest(".spectrum-Modal") != null
|
||||
const sourceInModal = handler.anchor.closest(".spectrum-Underlay") != null
|
||||
const clickInModal = event.target.closest(".spectrum-Underlay") != null
|
||||
if (clickInModal && !sourceInModal) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import "@spectrum-css/button/dist/index-vars.css"
|
||||
import Tooltip from "../Tooltip/Tooltip.svelte"
|
||||
|
||||
export let type
|
||||
export let disabled = false
|
||||
export let size = "M"
|
||||
export let cta = false
|
||||
|
@ -21,6 +22,7 @@
|
|||
|
||||
<button
|
||||
{id}
|
||||
{type}
|
||||
class:spectrum-Button--cta={cta}
|
||||
class:spectrum-Button--primary={primary}
|
||||
class:spectrum-Button--secondary={secondary}
|
||||
|
@ -73,6 +75,7 @@
|
|||
button {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.spectrum-Button-label {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
|
|
@ -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>
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import Icon from "../Icon/Icon.svelte"
|
||||
import { getContext, onMount } from "svelte"
|
||||
import { slide } from "svelte/transition"
|
||||
import ErrorMessage from "./ErrorMessage.svelte"
|
||||
|
||||
export let disabled = false
|
||||
export let error = null
|
||||
|
@ -55,9 +55,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
{#if error}
|
||||
<div transition:slide|local={{ duration: 130 }} class="error-message">
|
||||
{error}
|
||||
</div>
|
||||
<ErrorMessage {error} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
@ -110,13 +108,6 @@
|
|||
.field {
|
||||
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 {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
|
|
@ -4,3 +4,4 @@ export { default as FancySelect } from "./FancySelect.svelte"
|
|||
export { default as FancyButton } from "./FancyButton.svelte"
|
||||
export { default as FancyForm } from "./FancyForm.svelte"
|
||||
export { default as FancyButtonRadio } from "./FancyButtonRadio.svelte"
|
||||
export { default as ErrorMessage } from "./ErrorMessage.svelte"
|
||||
|
|
|
@ -18,10 +18,14 @@
|
|||
export let ignoreTimezones = false
|
||||
export let time24hr = false
|
||||
export let range = false
|
||||
export let flatpickr
|
||||
export let useKeyboardShortcuts = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const flatpickrId = `${uuid()}-wrapper`
|
||||
|
||||
let open = false
|
||||
let flatpickr, flatpickrOptions
|
||||
let flatpickrOptions
|
||||
|
||||
// Another classic flatpickr issue. Errors were randomly being thrown due to
|
||||
// flatpickr internal code. Making sure that "destroy" is a valid function
|
||||
|
@ -59,6 +63,8 @@
|
|||
dispatch("change", timestamp.toISOString())
|
||||
}
|
||||
},
|
||||
onOpen: () => dispatch("open"),
|
||||
onClose: () => dispatch("close"),
|
||||
}
|
||||
|
||||
$: redrawOptions = {
|
||||
|
@ -113,12 +119,16 @@
|
|||
|
||||
const onOpen = () => {
|
||||
open = true
|
||||
document.addEventListener("keyup", clearDateOnBackspace)
|
||||
if (useKeyboardShortcuts) {
|
||||
document.addEventListener("keyup", clearDateOnBackspace)
|
||||
}
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
open = false
|
||||
document.removeEventListener("keyup", clearDateOnBackspace)
|
||||
if (useKeyboardShortcuts) {
|
||||
document.removeEventListener("keyup", clearDateOnBackspace)
|
||||
}
|
||||
|
||||
// Manually blur all input fields since flatpickr creates a second
|
||||
// duplicate input field.
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 26 KiB |
|
@ -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 |
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/builder",
|
||||
"version": "2.5.6-alpha.30",
|
||||
"version": "0.0.1",
|
||||
"license": "GPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -58,11 +58,11 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "2.5.6-alpha.30",
|
||||
"@budibase/client": "2.5.6-alpha.30",
|
||||
"@budibase/frontend-core": "2.5.6-alpha.30",
|
||||
"@budibase/shared-core": "2.5.6-alpha.30",
|
||||
"@budibase/string-templates": "2.5.6-alpha.30",
|
||||
"@budibase/bbui": "0.0.1",
|
||||
"@budibase/frontend-core": "0.0.1",
|
||||
"@budibase/shared-core": "0.0.1",
|
||||
"@budibase/string-templates": "0.0.1",
|
||||
"@budibase/types": "0.0.1",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||
|
|
|
@ -147,6 +147,9 @@ const automationActions = store => ({
|
|||
testData,
|
||||
})
|
||||
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"
|
||||
}
|
||||
store.update(state => {
|
||||
|
|
|
@ -134,6 +134,7 @@ export const getFrontendStore = () => {
|
|||
previousTopNavPath: {},
|
||||
version: application.version,
|
||||
revertableVersion: application.revertableVersion,
|
||||
upgradableVersion: application.upgradableVersion,
|
||||
navigation: application.navigation || {},
|
||||
usedPlugins: application.usedPlugins || [],
|
||||
}))
|
||||
|
|
|
@ -94,7 +94,7 @@
|
|||
/>
|
||||
<span class="icon-spacing">
|
||||
<Body size="XS">
|
||||
{idx.charAt(0).toUpperCase() + idx.slice(1)}
|
||||
{action.stepTitle || idx.charAt(0).toUpperCase() + idx.slice(1)}
|
||||
</Body>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import DiscordLogo from "assets/discord.svg"
|
||||
import ZapierLogo from "assets/zapier.png"
|
||||
import IntegromatLogo from "assets/integromat.png"
|
||||
import MakeLogo from "assets/make.svg"
|
||||
import SlackLogo from "assets/slack.svg"
|
||||
|
||||
export const externalActions = {
|
||||
zapier: { name: "zapier", icon: ZapierLogo },
|
||||
discord: { name: "discord", icon: DiscordLogo },
|
||||
slack: { name: "slack", icon: SlackLogo },
|
||||
integromat: { name: "integromat", icon: IntegromatLogo },
|
||||
integromat: { name: "integromat", icon: MakeLogo },
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
await automationStore.actions.test($selectedAutomation, testData)
|
||||
$automationStore.showTestPanel = true
|
||||
} catch (error) {
|
||||
notifications.error("Error testing automation")
|
||||
notifications.error(error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -61,11 +61,63 @@
|
|||
$: isTrigger = block?.type === "TRIGGER"
|
||||
$: 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) => {
|
||||
let newInputData = testData || blockInputs
|
||||
if (block.event === "app:trigger" && !newInputData?.fields) {
|
||||
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
|
||||
setDefaultEnumValues()
|
||||
}
|
||||
|
@ -239,7 +291,7 @@
|
|||
</script>
|
||||
|
||||
<div class="fields">
|
||||
{#each schemaProperties as [key, value]}
|
||||
{#each deprecatedSchemaProperties as [key, value]}
|
||||
<div class="block-field">
|
||||
{#if key !== "fields"}
|
||||
<Label
|
||||
|
@ -256,6 +308,28 @@
|
|||
options={value.enum}
|
||||
getOptionLabel={(x, idx) => (value.pretty ? value.pretty[idx] : x)}
|
||||
/>
|
||||
{:else if value.type === "json"}
|
||||
<Editor
|
||||
editorHeight="250"
|
||||
editorWidth="448"
|
||||
mode="json"
|
||||
value={inputData[key]?.value}
|
||||
on:change={e => {
|
||||
/**
|
||||
* TODO - Remove after November 2023
|
||||
* *******************************
|
||||
* Code added to provide backwards compatibility between Values 1,2,3,4,5
|
||||
* and the new JSON body.
|
||||
*/
|
||||
delete inputData.value1
|
||||
delete inputData.value2
|
||||
delete inputData.value3
|
||||
delete inputData.value4
|
||||
delete inputData.value5
|
||||
/***********************/
|
||||
onChange(e, key)
|
||||
}}
|
||||
/>
|
||||
{:else if value.customType === "column"}
|
||||
<Select
|
||||
on:change={e => onChange(e, key)}
|
||||
|
|
|
@ -22,6 +22,8 @@
|
|||
export let rowCount
|
||||
export let disableSorting = false
|
||||
export let customPlaceholder = false
|
||||
export let allowClickRows
|
||||
export let allowEditing = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -109,7 +111,9 @@
|
|||
{rowCount}
|
||||
{disableSorting}
|
||||
{customPlaceholder}
|
||||
allowEditRows={allowEditing}
|
||||
showAutoColumns={!hideAutocolumns}
|
||||
{allowClickRows}
|
||||
on:clickrelationship={e => selectRelationship(e.detail)}
|
||||
on:sort
|
||||
>
|
||||
|
|
|
@ -58,6 +58,7 @@
|
|||
{loading}
|
||||
{type}
|
||||
rowCount={10}
|
||||
allowEditing={false}
|
||||
bind:hideAutocolumns
|
||||
>
|
||||
<ViewFilterButton {view} />
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
{#if datasource}
|
||||
<div>
|
||||
<ActionButton icon="DataCorrelated" primary quiet on:click={modal.show}>
|
||||
Define existing relationship
|
||||
Define relationship
|
||||
</ActionButton>
|
||||
</div>
|
||||
<Modal bind:this={modal}>
|
||||
|
|
|
@ -9,13 +9,21 @@
|
|||
$: selectedRowArray = Object.keys($selectedRows).map(id => ({ _id: id }))
|
||||
</script>
|
||||
|
||||
<ExportButton
|
||||
{disabled}
|
||||
view={$tableId}
|
||||
filters={$filter}
|
||||
sorting={{
|
||||
sortColumn: $sort.column,
|
||||
sortOrder: $sort.order,
|
||||
}}
|
||||
selectedRows={selectedRowArray}
|
||||
/>
|
||||
<span data-ignore-click-outside="true">
|
||||
<ExportButton
|
||||
{disabled}
|
||||
view={$tableId}
|
||||
filters={$filter}
|
||||
sorting={{
|
||||
sortColumn: $sort.column,
|
||||
sortOrder: $sort.order,
|
||||
}}
|
||||
selectedRows={selectedRowArray}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<style>
|
||||
span {
|
||||
display: contents;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -2,19 +2,19 @@
|
|||
import TableFilterButton from "../TableFilterButton.svelte"
|
||||
import { getContext } from "svelte"
|
||||
|
||||
const { columns, config, filter, table } = getContext("grid")
|
||||
const { columns, tableId, filter, table } = getContext("grid")
|
||||
|
||||
const onFilter = e => {
|
||||
filter.set(e.detail || [])
|
||||
}
|
||||
</script>
|
||||
|
||||
{#key $config.tableId}
|
||||
{#key $tableId}
|
||||
<TableFilterButton
|
||||
schema={$table?.schema}
|
||||
filters={$filter}
|
||||
on:change={onFilter}
|
||||
disabled={!$columns.length}
|
||||
tableId={$config.tableId}
|
||||
tableId={$tableId}
|
||||
/>
|
||||
{/key}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import ManageAccessButton from "../ManageAccessButton.svelte"
|
||||
import { getContext } from "svelte"
|
||||
|
||||
const { config } = getContext("grid")
|
||||
const { tableId } = getContext("grid")
|
||||
</script>
|
||||
|
||||
<ManageAccessButton resourceId={$config.tableId} />
|
||||
<ManageAccessButton resourceId={$tableId} />
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
import NavItem from "components/common/NavItem.svelte"
|
||||
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
|
||||
|
||||
|
|
|
@ -42,16 +42,7 @@ export const parseFile = e => {
|
|||
|
||||
reader.addEventListener("load", function (e) {
|
||||
const fileData = e.target.result
|
||||
|
||||
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") {
|
||||
if (file.type?.includes("json")) {
|
||||
const parsedFileData = JSON.parse(fileData)
|
||||
|
||||
if (Array.isArray(parsedFileData)) {
|
||||
|
@ -62,7 +53,13 @@ export const parseFile = e => {
|
|||
reject("invalid json format")
|
||||
}
|
||||
} else {
|
||||
reject("invalid file type")
|
||||
API.csvToJson(fileData)
|
||||
.then(rows => {
|
||||
resolveRows(rows)
|
||||
})
|
||||
.catch(() => {
|
||||
reject("cannot parse csv")
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
export let highlighted = false
|
||||
export let rightAlignIcon = false
|
||||
export let id
|
||||
export let showTooltip = false
|
||||
|
||||
const scrollApi = getContext("scroll")
|
||||
const dispatch = createEventDispatcher()
|
||||
|
@ -84,7 +85,7 @@
|
|||
<Icon color={iconColor} size="S" name={icon} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="text">{text}</div>
|
||||
<div class="text" title={showTooltip ? text : null}>{text}</div>
|
||||
{#if withActions}
|
||||
<div class="actions">
|
||||
<slot />
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
} from "@budibase/bbui"
|
||||
import { store } from "builderStore"
|
||||
import { API } from "api"
|
||||
import clientPackage from "@budibase/client/package.json"
|
||||
|
||||
export function show() {
|
||||
updateModal.show()
|
||||
|
@ -25,9 +24,9 @@
|
|||
|
||||
$: appId = $store.appId
|
||||
$: updateAvailable =
|
||||
clientPackage.version &&
|
||||
$store.upgradableVersion &&
|
||||
$store.version &&
|
||||
clientPackage.version !== $store.version
|
||||
$store.upgradableVersion !== $store.version
|
||||
$: revertAvailable = $store.revertableVersion != null
|
||||
|
||||
const refreshAppPackage = async () => {
|
||||
|
@ -46,7 +45,7 @@
|
|||
// Don't wait for the async refresh, since this causes modal flashing
|
||||
refreshAppPackage()
|
||||
notifications.success(
|
||||
`App updated successfully to version ${clientPackage.version}`
|
||||
`App updated successfully to version ${$store.upgradableVersion}`
|
||||
)
|
||||
} catch (err) {
|
||||
notifications.error(`Error updating app: ${err}`)
|
||||
|
@ -91,7 +90,7 @@
|
|||
{#if updateAvailable}
|
||||
<Body size="S">
|
||||
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.
|
||||
</Body>
|
||||
{:else}
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
export let tab = true
|
||||
export let mode
|
||||
export let editorHeight = 500
|
||||
export let editorWidth = 640
|
||||
// export let parameters = []
|
||||
|
||||
let width
|
||||
|
@ -169,7 +170,9 @@
|
|||
{#if label}
|
||||
<Label small>{label}</Label>
|
||||
{/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} />
|
||||
</div>
|
||||
|
||||
|
@ -183,6 +186,7 @@
|
|||
}
|
||||
|
||||
div :global(.CodeMirror) {
|
||||
width: var(--code-mirror-width) !important;
|
||||
height: var(--code-mirror-height) !important;
|
||||
border-radius: var(--border-radius-s);
|
||||
font-family: var(--font-mono);
|
||||
|
|
|
@ -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>
|
|
@ -3,7 +3,6 @@ import { temporalStore } from "builderStore"
|
|||
import { admin, auth, licensing } from "stores/portal"
|
||||
import { get } from "svelte/store"
|
||||
import { BANNER_TYPES } from "@budibase/bbui"
|
||||
import { capitalise } from "helpers"
|
||||
|
||||
const oneDayInSeconds = 86400
|
||||
|
||||
|
@ -146,20 +145,19 @@ const buildUsersAboveLimitBanner = EXPIRY_KEY => {
|
|||
const userLicensing = get(licensing)
|
||||
return {
|
||||
key: EXPIRY_KEY,
|
||||
type: BANNER_TYPES.WARNING,
|
||||
criteria: () => {
|
||||
return userLicensing.warnUserLimit
|
||||
type: BANNER_TYPES.NEGATIVE,
|
||||
onChange: () => {
|
||||
defaultCacheFn(EXPIRY_KEY)
|
||||
},
|
||||
message: `${capitalise(
|
||||
userLicensing.license.plan.type
|
||||
)} plan changes - Users will be limited to ${
|
||||
userLicensing.userLimit
|
||||
} users in ${userLicensing.userLimitDays}`,
|
||||
criteria: () => {
|
||||
return userLicensing.errUserLimit
|
||||
},
|
||||
message: "Your Budibase account is de-activated. Upgrade your plan",
|
||||
...{
|
||||
extraButtonText: "Find out more",
|
||||
extraButtonText: "View plans",
|
||||
extraButtonAction: () => {
|
||||
defaultCacheFn(ExpiringKeys.LICENSING_USERS_ABOVE_LIMIT_BANNER)
|
||||
window.location.href = "/builder/portal/users/users"
|
||||
window.location.href = "https://budibase.com/pricing/"
|
||||
},
|
||||
},
|
||||
showCloseButton: true,
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
|
||||
export let app
|
||||
|
||||
export let lockedAction
|
||||
|
||||
const handleDefaultClick = () => {
|
||||
if (window.innerWidth < 640) {
|
||||
goToOverview()
|
||||
|
@ -29,7 +31,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="app-row" on:click={handleDefaultClick}>
|
||||
<div class="app-row" on:click={lockedAction || handleDefaultClick}>
|
||||
<div class="title">
|
||||
<div class="app-icon">
|
||||
<Icon size="L" name={app.icon?.name || "Apps"} color={app.icon?.color} />
|
||||
|
@ -58,8 +60,11 @@
|
|||
|
||||
<div class="app-row-actions">
|
||||
<AppLockModal {app} buttonSize="M" />
|
||||
<Button size="S" secondary on:click={goToOverview}>Manage</Button>
|
||||
<Button size="S" primary on:click={goToBuilder}>Edit</Button>
|
||||
<Button size="S" secondary on:click={lockedAction || goToOverview}
|
||||
>Manage</Button
|
||||
>
|
||||
<Button size="S" primary on:click={lockedAction || goToBuilder}>Edit</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
onMount(() => {
|
||||
unlimited = isUnlimited()
|
||||
percentage = getPercentage()
|
||||
if (warnWhenFull && percentage === 100) {
|
||||
if (warnWhenFull && percentage >= 100) {
|
||||
showWarning = true
|
||||
}
|
||||
})
|
||||
|
|
|
@ -28,13 +28,16 @@
|
|||
let inviting = false
|
||||
let searchFocus = false
|
||||
|
||||
// Initially filter entities without app access
|
||||
// Show all when false
|
||||
let filterByAppAccess = true
|
||||
|
||||
let appInvites = []
|
||||
let filteredInvites = []
|
||||
let filteredUsers = []
|
||||
let filteredGroups = []
|
||||
let selectedGroup
|
||||
let userOnboardResponse = null
|
||||
|
||||
let userLimitReachedModal
|
||||
|
||||
$: queryIsEmail = emailValidator(query) === true
|
||||
|
@ -52,15 +55,32 @@
|
|||
}
|
||||
|
||||
const filterInvites = async query => {
|
||||
appInvites = await getInvites()
|
||||
if (!query || query == "") {
|
||||
filteredInvites = appInvites
|
||||
if (!prodAppId) {
|
||||
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
|
||||
}
|
||||
|
||||
filteredInvites = appInvites.filter(invite => {
|
||||
const inviteInfo = invite.info?.apps
|
||||
if (!query && inviteInfo && prodAppId) {
|
||||
return Object.keys(inviteInfo).includes(prodAppId)
|
||||
}
|
||||
return invite.email.includes(query)
|
||||
})
|
||||
}
|
||||
|
||||
$: filterInvites(query)
|
||||
$: filterByAppAccess, prodAppId, filterInvites(query)
|
||||
$: if (searchFocus === true) {
|
||||
filterByAppAccess = false
|
||||
}
|
||||
|
||||
const usersFetch = fetchData({
|
||||
API,
|
||||
|
@ -79,9 +99,9 @@
|
|||
}
|
||||
await usersFetch.update({
|
||||
query: {
|
||||
appId: query ? null : prodAppId,
|
||||
appId: query || !filterByAppAccess ? null : prodAppId,
|
||||
email: query,
|
||||
paginated: query ? null : false,
|
||||
paginated: query || !filterByAppAccess ? null : false,
|
||||
},
|
||||
})
|
||||
await usersFetch.refresh()
|
||||
|
@ -107,7 +127,12 @@
|
|||
}
|
||||
|
||||
const debouncedUpdateFetch = Utils.debounce(searchUsers, 250)
|
||||
$: debouncedUpdateFetch(query, $store.builderSidePanel, loaded)
|
||||
$: debouncedUpdateFetch(
|
||||
query,
|
||||
$store.builderSidePanel,
|
||||
loaded,
|
||||
filterByAppAccess
|
||||
)
|
||||
|
||||
const updateAppUser = async (user, role) => {
|
||||
if (!prodAppId) {
|
||||
|
@ -182,9 +207,10 @@
|
|||
}
|
||||
|
||||
const searchGroups = (userGroups, query) => {
|
||||
let filterGroups = query?.length
|
||||
? userGroups
|
||||
: getAppGroups(userGroups, prodAppId)
|
||||
let filterGroups =
|
||||
query?.length || !filterByAppAccess
|
||||
? userGroups
|
||||
: getAppGroups(userGroups, prodAppId)
|
||||
return filterGroups
|
||||
.filter(group => {
|
||||
if (!query?.length) {
|
||||
|
@ -214,7 +240,7 @@
|
|||
}
|
||||
|
||||
// Adds the 'role' attribute and sets it to the current app.
|
||||
$: enrichedGroups = getEnrichedGroups($groups)
|
||||
$: enrichedGroups = getEnrichedGroups($groups, filterByAppAccess)
|
||||
$: filteredGroups = searchGroups(enrichedGroups, query)
|
||||
$: groupUsers = buildGroupUsers(filteredGroups, filteredUsers)
|
||||
$: allUsers = [...filteredUsers, ...groupUsers]
|
||||
|
@ -226,7 +252,7 @@
|
|||
specific roles for the app.
|
||||
*/
|
||||
const buildGroupUsers = (userGroups, filteredUsers) => {
|
||||
if (query) {
|
||||
if (query || !filterByAppAccess) {
|
||||
return []
|
||||
}
|
||||
// Must exclude users who have explicit privileges
|
||||
|
@ -321,12 +347,12 @@
|
|||
[prodAppId]: role,
|
||||
},
|
||||
})
|
||||
await filterInvites()
|
||||
await filterInvites(query)
|
||||
}
|
||||
|
||||
const onUninviteAppUser = async 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?
|
||||
|
@ -351,7 +377,6 @@
|
|||
|
||||
onMount(() => {
|
||||
rendered = true
|
||||
searchFocus = true
|
||||
})
|
||||
|
||||
function handleKeyDown(evt) {
|
||||
|
@ -417,7 +442,6 @@
|
|||
autocomplete="off"
|
||||
disabled={inviting}
|
||||
value={query}
|
||||
autofocus
|
||||
on:input={e => {
|
||||
query = e.target.value.trim()
|
||||
}}
|
||||
|
@ -428,16 +452,20 @@
|
|||
|
||||
<span
|
||||
class="search-input-icon"
|
||||
class:searching={query}
|
||||
class:searching={query || !filterByAppAccess}
|
||||
on:click={() => {
|
||||
if (!filterByAppAccess) {
|
||||
filterByAppAccess = true
|
||||
}
|
||||
if (!query) {
|
||||
return
|
||||
}
|
||||
query = null
|
||||
userOnboardResponse = null
|
||||
filterByAppAccess = true
|
||||
}}
|
||||
>
|
||||
<Icon name={query ? "Close" : "Search"} />
|
||||
<Icon name={!filterByAppAccess || query ? "Close" : "Search"} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
@ -555,7 +583,7 @@
|
|||
|
||||
{#if filteredUsers?.length}
|
||||
<div class="auth-entity-section">
|
||||
<div class="auth-entity-header ">
|
||||
<div class="auth-entity-header">
|
||||
<div class="auth-entity-title">Users</div>
|
||||
<div class="auth-entity-access-title">Access</div>
|
||||
</div>
|
||||
|
@ -696,7 +724,7 @@
|
|||
max-width: calc(100vw - 40px);
|
||||
background: var(--background);
|
||||
border-left: var(--border-light);
|
||||
z-index: 3;
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<Panel title={$selectedLayout?.name} icon="Experience" borderLeft>
|
||||
<Panel title={$selectedLayout?.name} icon="Experience" borderLeft wide>
|
||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
||||
<Banner type="warning" showCloseButton={false}>
|
||||
Custom layouts are being deprecated. They will be removed in a future
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<Panel borderLeft title="Navigation" icon="InfoOutline">
|
||||
<Panel borderLeft title="Navigation" icon="InfoOutline" wide>
|
||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
||||
{#if $selectedScreen.layoutId}
|
||||
<Banner
|
||||
|
|
|
@ -59,6 +59,7 @@
|
|||
text={screen.routing.route}
|
||||
on:click={() => store.actions.screens.select(screen._id)}
|
||||
rightAlignIcon
|
||||
showTooltip
|
||||
>
|
||||
<ScreenDropdownMenu screenId={screen._id} />
|
||||
<RoleIndicator slot="right" roleId={screen.routing.roleId} />
|
||||
|
|
|
@ -149,6 +149,7 @@
|
|||
title={$selectedScreen.routing.route}
|
||||
icon={$selectedScreen.routing.route === "/" ? "Home" : "WebPage"}
|
||||
borderLeft
|
||||
wide
|
||||
>
|
||||
<Layout gap="S" paddingX="L" paddingY="XL">
|
||||
{#if $selectedScreen.layoutId}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import { Body, Layout } from "@budibase/bbui"
|
||||
</script>
|
||||
|
||||
<Panel borderLeft title="Theme" icon="InfoOutline">
|
||||
<Panel borderLeft title="Theme" icon="InfoOutline" wide>
|
||||
<Layout paddingX="L" paddingY="XL">
|
||||
<Body size="S">
|
||||
Your theme is set across all the screens within your app.
|
||||
|
|
|
@ -133,7 +133,7 @@
|
|||
</Body>
|
||||
</Layout>
|
||||
<Divider />
|
||||
{#if $licensing.usageMetrics?.dayPasses >= 100}
|
||||
{#if $licensing.usageMetrics?.dayPasses >= 100 || $licensing.errUserLimit}
|
||||
<div>
|
||||
<Layout gap="S" justifyItems="center">
|
||||
<img class="spaceman" alt="spaceman" src={Spaceman} />
|
||||
|
|
|
@ -43,12 +43,18 @@
|
|||
}
|
||||
|
||||
$: quotaUsage = $licensing.quotaUsage
|
||||
|
||||
$: license = $auth.user?.license
|
||||
$: plan = license?.plan
|
||||
$: usesInvoicing = plan?.usesInvoicing
|
||||
|
||||
$: accountPortalAccess = $auth?.user?.accountPortalAccess
|
||||
$: quotaReset = quotaUsage?.quotaReset
|
||||
$: canManagePlan =
|
||||
($admin.cloud && accountPortalAccess) || (!$admin.cloud && $auth.isAdmin)
|
||||
|
||||
$: showButton = !usesInvoicing && accountPortalAccess
|
||||
|
||||
const setMonthlyUsage = () => {
|
||||
monthlyUsage = []
|
||||
if (quotaUsage.monthly) {
|
||||
|
@ -121,7 +127,7 @@
|
|||
const setTextRows = () => {
|
||||
textRows = []
|
||||
|
||||
if (cancelAt) {
|
||||
if (cancelAt && !usesInvoicing) {
|
||||
textRows.push({ message: "Subscription has been cancelled" })
|
||||
textRows.push({
|
||||
message: `${getDaysRemaining(cancelAt)} days remaining`,
|
||||
|
@ -213,7 +219,7 @@
|
|||
description="YOUR CURRENT PLAN"
|
||||
title={planTitle()}
|
||||
{primaryActionText}
|
||||
primaryAction={accountPortalAccess ? goToAccountPortal : undefined}
|
||||
primaryAction={showButton ? goToAccountPortal : undefined}
|
||||
{textRows}
|
||||
>
|
||||
<div class="content">
|
||||
|
@ -224,33 +230,23 @@
|
|||
<Usage {usage} warnWhenFull={WARN_USAGE.includes(usage.name)} />
|
||||
</div>
|
||||
{/each}
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="S">Monthly limits</Heading>
|
||||
<div class="detail">
|
||||
<TooltipWrapper tooltip={new Date(quotaReset)}>
|
||||
<Detail size="M">
|
||||
Resets in {daysRemainingInMonth} days
|
||||
</Detail>
|
||||
</TooltipWrapper>
|
||||
</div>
|
||||
</Layout>
|
||||
<Layout noPadding gap="M">
|
||||
{#each monthlyUsage as usage}
|
||||
<Usage {usage} warnWhenFull={WARN_USAGE.includes(usage.name)} />
|
||||
{/each}
|
||||
</Layout>
|
||||
</Layout>
|
||||
</div>
|
||||
|
||||
{#if monthlyUsage.length}
|
||||
<div class="column">
|
||||
<Layout noPadding gap="M">
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="S">Monthly limits</Heading>
|
||||
<div class="detail">
|
||||
<TooltipWrapper tooltip={new Date(quotaReset)}>
|
||||
<Detail size="M">
|
||||
Resets in {daysRemainingInMonth} days
|
||||
</Detail>
|
||||
</TooltipWrapper>
|
||||
</div>
|
||||
</Layout>
|
||||
<Layout noPadding gap="M">
|
||||
{#each monthlyUsage as usage}
|
||||
<Usage
|
||||
{usage}
|
||||
warnWhenFull={WARN_USAGE.includes(usage.name)}
|
||||
/>
|
||||
{/each}
|
||||
</Layout>
|
||||
</Layout>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</DashCard>
|
||||
</Layout>
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
import Spinner from "components/common/Spinner.svelte"
|
||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||
import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte"
|
||||
import AccountLockedModal from "components/portal/licensing/AccountLockedModal.svelte"
|
||||
|
||||
import { store, automationStore } from "builderStore"
|
||||
import { API } from "api"
|
||||
|
@ -28,6 +29,7 @@
|
|||
let template
|
||||
let creationModal
|
||||
let appLimitModal
|
||||
let accountLockedModal
|
||||
let creatingApp = false
|
||||
let searchTerm = ""
|
||||
let creatingFromTemplate = false
|
||||
|
@ -48,6 +50,11 @@
|
|||
: true)
|
||||
)
|
||||
$: automationErrors = getAutomationErrors(enrichedApps)
|
||||
$: isOwner = $auth.accountPortalAccess && $admin.cloud
|
||||
|
||||
const usersLimitLockAction = $licensing?.errUserLimit
|
||||
? () => accountLockedModal.show()
|
||||
: null
|
||||
|
||||
const enrichApps = (apps, user, sortBy) => {
|
||||
const enrichedApps = apps.map(app => ({
|
||||
|
@ -189,6 +196,9 @@
|
|||
creatingFromTemplate = true
|
||||
createAppFromTemplateUrl(initInfo.init_template)
|
||||
}
|
||||
if (usersLimitLockAction) {
|
||||
usersLimitLockAction()
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error("Error getting init info")
|
||||
}
|
||||
|
@ -230,20 +240,30 @@
|
|||
<Layout noPadding gap="L">
|
||||
<div class="title">
|
||||
<div class="buttons">
|
||||
<Button size="M" cta on:click={initiateAppCreation}>
|
||||
<Button
|
||||
size="M"
|
||||
cta
|
||||
on:click={usersLimitLockAction || initiateAppCreation}
|
||||
>
|
||||
Create new app
|
||||
</Button>
|
||||
{#if $apps?.length > 0}
|
||||
<Button
|
||||
size="M"
|
||||
secondary
|
||||
on:click={$goto("/builder/portal/apps/templates")}
|
||||
on:click={usersLimitLockAction ||
|
||||
$goto("/builder/portal/apps/templates")}
|
||||
>
|
||||
View templates
|
||||
</Button>
|
||||
{/if}
|
||||
{#if !$apps?.length}
|
||||
<Button size="L" quiet secondary on:click={initiateAppImport}>
|
||||
<Button
|
||||
size="L"
|
||||
quiet
|
||||
secondary
|
||||
on:click={usersLimitLockAction || initiateAppImport}
|
||||
>
|
||||
Import app
|
||||
</Button>
|
||||
{/if}
|
||||
|
@ -267,7 +287,7 @@
|
|||
|
||||
<div class="app-table">
|
||||
{#each filteredApps as app (app.appId)}
|
||||
<AppRow {app} />
|
||||
<AppRow {app} lockedAction={usersLimitLockAction} />
|
||||
{/each}
|
||||
</div>
|
||||
</Layout>
|
||||
|
@ -294,6 +314,11 @@
|
|||
</Modal>
|
||||
|
||||
<AppLimitModal bind:this={appLimitModal} />
|
||||
<AccountLockedModal
|
||||
bind:this={accountLockedModal}
|
||||
onConfirm={() =>
|
||||
isOwner ? $licensing.goToUpgradePage() : $licensing.goToPricingPage()}
|
||||
/>
|
||||
|
||||
<style>
|
||||
.title {
|
||||
|
|
|
@ -107,8 +107,9 @@
|
|||
useSampleData,
|
||||
isGoogle,
|
||||
}) => {
|
||||
let app
|
||||
try {
|
||||
const app = await createApp(useSampleData)
|
||||
app = await createApp(useSampleData)
|
||||
|
||||
let datasource
|
||||
if (datasourceConfig) {
|
||||
|
@ -134,6 +135,17 @@
|
|||
console.log(e)
|
||||
creationLoading = false
|
||||
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>
|
||||
|
@ -146,80 +158,87 @@
|
|||
/>
|
||||
</Modal>
|
||||
|
||||
<SplitPage>
|
||||
{#if stage === "name"}
|
||||
<NamePanel bind:name bind:url onNext={() => (stage = "data")} />
|
||||
{:else if googleComplete}
|
||||
<div class="centered">
|
||||
<Body
|
||||
>Please login to your Google account in the new tab which as opened to
|
||||
continue.</Body
|
||||
>
|
||||
</div>
|
||||
{:else if integrationsLoading || creationLoading}
|
||||
<div class="centered">
|
||||
<Spinner />
|
||||
</div>
|
||||
{:else if stage === "data"}
|
||||
<DataPanel onBack={() => (stage = "name")}>
|
||||
<div class="dataButton">
|
||||
<FancyButton on:click={() => handleCreateApp({ useSampleData: true })}>
|
||||
<div class="dataButtonContent">
|
||||
<div class="dataButtonIcon">
|
||||
<img
|
||||
alt="Budibase Logo"
|
||||
class="budibaseLogo"
|
||||
src={"https://i.imgur.com/Xhdt1YP.png"}
|
||||
/>
|
||||
</div>
|
||||
Budibase Sample data
|
||||
</div>
|
||||
</FancyButton>
|
||||
<div class="full-width">
|
||||
<SplitPage>
|
||||
{#if stage === "name"}
|
||||
<NamePanel bind:name bind:url onNext={() => (stage = "data")} />
|
||||
{:else if googleComplete}
|
||||
<div class="centered">
|
||||
<Body
|
||||
>Please login to your Google account in the new tab which as opened to
|
||||
continue.</Body
|
||||
>
|
||||
</div>
|
||||
<div class="dataButton">
|
||||
<FancyButton on:click={uploadModal.show}>
|
||||
<div class="dataButtonContent">
|
||||
<div class="dataButtonIcon">
|
||||
<FontAwesomeIcon name="fa-solid fa-file-arrow-up" />
|
||||
</div>
|
||||
Upload data (CSV or JSON)
|
||||
</div>
|
||||
</FancyButton>
|
||||
{:else if integrationsLoading || creationLoading}
|
||||
<div class="centered">
|
||||
<Spinner />
|
||||
</div>
|
||||
{#each Object.entries(plusIntegrations) as [integrationType, schema]}
|
||||
{:else if stage === "data"}
|
||||
<DataPanel onBack={() => (stage = "name")}>
|
||||
<div class="dataButton">
|
||||
<FancyButton on:click={() => (stage = integrationType)}>
|
||||
<FancyButton
|
||||
on:click={() => handleCreateApp({ useSampleData: true })}
|
||||
>
|
||||
<div class="dataButtonContent">
|
||||
<div class="dataButtonIcon">
|
||||
<IntegrationIcon {integrationType} {schema} />
|
||||
<img
|
||||
alt="Budibase Logo"
|
||||
class="budibaseLogo"
|
||||
src={"https://i.imgur.com/Xhdt1YP.png"}
|
||||
/>
|
||||
</div>
|
||||
{schema.friendlyName}
|
||||
Budibase Sample data
|
||||
</div>
|
||||
</FancyButton>
|
||||
</div>
|
||||
{/each}
|
||||
</DataPanel>
|
||||
{:else if stage in plusIntegrations}
|
||||
<DatasourceConfigPanel
|
||||
title={plusIntegrations[stage].friendlyName}
|
||||
fields={plusIntegrations[stage].datasource}
|
||||
type={stage}
|
||||
onBack={() => (stage = "data")}
|
||||
onNext={data => {
|
||||
const isGoogle = data.isGoogle
|
||||
delete data.isGoogle
|
||||
return handleCreateApp({ datasourceConfig: data, isGoogle })
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<p>There was an problem. Please refresh the page and try again.</p>
|
||||
{/if}
|
||||
<div slot="right">
|
||||
<ExampleApp {name} showData={stage !== "name"} />
|
||||
</div>
|
||||
</SplitPage>
|
||||
<div class="dataButton">
|
||||
<FancyButton on:click={uploadModal.show}>
|
||||
<div class="dataButtonContent">
|
||||
<div class="dataButtonIcon">
|
||||
<FontAwesomeIcon name="fa-solid fa-file-arrow-up" />
|
||||
</div>
|
||||
Upload data (CSV or JSON)
|
||||
</div>
|
||||
</FancyButton>
|
||||
</div>
|
||||
{#each Object.entries(plusIntegrations) as [integrationType, schema]}
|
||||
<div class="dataButton">
|
||||
<FancyButton on:click={() => (stage = integrationType)}>
|
||||
<div class="dataButtonContent">
|
||||
<div class="dataButtonIcon">
|
||||
<IntegrationIcon {integrationType} {schema} />
|
||||
</div>
|
||||
{schema.friendlyName}
|
||||
</div>
|
||||
</FancyButton>
|
||||
</div>
|
||||
{/each}
|
||||
</DataPanel>
|
||||
{:else if stage in plusIntegrations}
|
||||
<DatasourceConfigPanel
|
||||
title={plusIntegrations[stage].friendlyName}
|
||||
fields={plusIntegrations[stage].datasource}
|
||||
type={stage}
|
||||
onBack={() => (stage = "data")}
|
||||
onNext={data => {
|
||||
const isGoogle = data.isGoogle
|
||||
delete data.isGoogle
|
||||
return handleCreateApp({ datasourceConfig: data, isGoogle })
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<p>There was an problem. Please refresh the page and try again.</p>
|
||||
{/if}
|
||||
<div slot="right">
|
||||
<ExampleApp {name} showData={stage !== "name"} />
|
||||
</div>
|
||||
</SplitPage>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
.centered {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
|
|
@ -176,7 +176,7 @@
|
|||
<Heading>Backups</Heading>
|
||||
{#if !$licensing.backupsEnabled}
|
||||
<Tags>
|
||||
<Tag icon="LockClosed">Pro plan</Tag>
|
||||
<Tag icon="LockClosed">Premium</Tag>
|
||||
</Tags>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { store } from "builderStore"
|
||||
import clientPackage from "@budibase/client/package.json"
|
||||
import { processStringSync } from "@budibase/string-templates"
|
||||
import { users, auth, apps, groups, overview } from "stores/portal"
|
||||
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
|
||||
$: appEditorId = !app?.updatedBy ? $auth.user._id : app?.updatedBy
|
||||
$: appEditorText = appEditor?.firstName || appEditor?.email
|
||||
|
@ -172,8 +171,8 @@
|
|||
<Heading size="XS">{$store.version}</Heading>
|
||||
{#if updateAvailable}
|
||||
<div class="version-status">
|
||||
New version <strong>{clientPackage.version}</strong> is available
|
||||
-
|
||||
New version <strong>{$store.upgradableVersion}</strong> is
|
||||
available -
|
||||
<Link
|
||||
on:click={() => {
|
||||
$goto("./version")
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
<script>
|
||||
import { Layout, Heading, Body, Divider, Button } from "@budibase/bbui"
|
||||
import { store } from "builderStore"
|
||||
import clientPackage from "@budibase/client/package.json"
|
||||
import VersionModal from "components/deploy/VersionModal.svelte"
|
||||
|
||||
let versionModal
|
||||
|
||||
$: updateAvailable = clientPackage.version !== $store.version
|
||||
$: updateAvailable = $store.upgradableVersion !== $store.version
|
||||
</script>
|
||||
|
||||
<Layout noPadding>
|
||||
|
@ -18,7 +17,7 @@
|
|||
{#if updateAvailable}
|
||||
<Body>
|
||||
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 />
|
||||
Updates can contain new features, performance improvements and bug fixes.
|
||||
</Body>
|
||||
|
|
|
@ -378,7 +378,7 @@
|
|||
</div>
|
||||
{#if !$licensing.enforceableSSO}
|
||||
<Tags>
|
||||
<Tag icon="LockClosed">Enterprise plan</Tag>
|
||||
<Tag icon="LockClosed">Enterprise</Tag>
|
||||
</Tags>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -213,7 +213,7 @@
|
|||
{/if}
|
||||
{#if isCloud && !brandingEnabled}
|
||||
<Tags>
|
||||
<Tag icon="LockClosed">Pro</Tag>
|
||||
<Tag icon="LockClosed">Premium</Tag>
|
||||
</Tags>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -1,47 +1,28 @@
|
|||
<script>
|
||||
import { url, goto } from "@roxi/routify"
|
||||
import {
|
||||
Button,
|
||||
Layout,
|
||||
ActionMenu,
|
||||
Heading,
|
||||
Icon,
|
||||
Popover,
|
||||
notifications,
|
||||
Table,
|
||||
ActionMenu,
|
||||
Layout,
|
||||
MenuItem,
|
||||
Modal,
|
||||
Table,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import UserGroupPicker from "components/settings/UserGroupPicker.svelte"
|
||||
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 { goto, url } from "@roxi/routify"
|
||||
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 GroupIcon from "./_components/GroupIcon.svelte"
|
||||
import { Breadcrumbs, Breadcrumb } from "components/portal/page"
|
||||
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"
|
||||
import GroupUsers from "./_components/GroupUsers.svelte"
|
||||
|
||||
export let groupId
|
||||
|
||||
$: userSchema = {
|
||||
email: {
|
||||
width: "1fr",
|
||||
},
|
||||
...(readonly
|
||||
? {}
|
||||
: {
|
||||
_id: {
|
||||
displayName: "",
|
||||
width: "auto",
|
||||
borderLeft: true,
|
||||
},
|
||||
}),
|
||||
}
|
||||
const appSchema = {
|
||||
name: {
|
||||
width: "2fr",
|
||||
|
@ -50,12 +31,6 @@
|
|||
width: "1fr",
|
||||
},
|
||||
}
|
||||
const customUserTableRenderers = [
|
||||
{
|
||||
column: "_id",
|
||||
component: RemoveUserTableRenderer,
|
||||
},
|
||||
]
|
||||
const customAppTableRenderers = [
|
||||
{
|
||||
column: "name",
|
||||
|
@ -67,20 +42,12 @@
|
|||
},
|
||||
]
|
||||
|
||||
let popoverAnchor
|
||||
let popover
|
||||
let searchTerm = ""
|
||||
let prevSearch = undefined
|
||||
let pageInfo = createPaginationStore()
|
||||
let loaded = false
|
||||
let editModal, deleteModal
|
||||
|
||||
$: scimEnabled = $features.isScimEnabled
|
||||
$: readonly = !$auth.isAdmin || scimEnabled
|
||||
$: page = $pageInfo.page
|
||||
$: fetchUsers(page, searchTerm)
|
||||
$: group = $groups.find(x => x._id === groupId)
|
||||
$: filtered = $users.data
|
||||
$: groupApps = $apps
|
||||
.filter(app =>
|
||||
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() {
|
||||
try {
|
||||
await groups.actions.delete(group)
|
||||
|
@ -130,21 +78,17 @@
|
|||
try {
|
||||
await groups.actions.save(group)
|
||||
} catch (error) {
|
||||
notifications.error(`Failed to save user group`)
|
||||
if (error.message) {
|
||||
notifications.error(error.message)
|
||||
} else {
|
||||
notifications.error(`Failed to save user group`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const removeUser = async id => {
|
||||
await groups.actions.removeUser(groupId, id)
|
||||
}
|
||||
|
||||
const removeApp = async app => {
|
||||
await groups.actions.removeApp(groupId, apps.getProdAppID(app.devId))
|
||||
}
|
||||
|
||||
setContext("users", {
|
||||
removeUser,
|
||||
})
|
||||
setContext("roles", {
|
||||
updateRole: () => {},
|
||||
removeRole: removeApp,
|
||||
|
@ -186,41 +130,7 @@
|
|||
</div>
|
||||
|
||||
<Layout noPadding gap="S">
|
||||
<div class="header">
|
||||
<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>
|
||||
<GroupUsers {groupId} />
|
||||
</Layout>
|
||||
|
||||
<Layout noPadding gap="S">
|
||||
|
|
|
@ -9,15 +9,23 @@
|
|||
|
||||
export let group
|
||||
export let saveGroup
|
||||
|
||||
let nameError
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
onConfirm={() => saveGroup(group)}
|
||||
onConfirm={() => {
|
||||
if (!group.name?.trim()) {
|
||||
nameError = "Group name cannot be empty"
|
||||
return false
|
||||
}
|
||||
saveGroup(group)
|
||||
}}
|
||||
size="M"
|
||||
title={group?._rev ? "Edit group" : "Create group"}
|
||||
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-inner">
|
||||
<Body size="XS">Icon</Body>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -66,6 +66,8 @@
|
|||
} catch (error) {
|
||||
if (error.status === 400) {
|
||||
notifications.error(error.message)
|
||||
} else if (error.message) {
|
||||
notifications.error(error.message)
|
||||
} else {
|
||||
notifications.error(`Failed to save group`)
|
||||
}
|
||||
|
@ -94,7 +96,7 @@
|
|||
<Heading size="M">Groups</Heading>
|
||||
{#if !$licensing.groupsEnabled}
|
||||
<Tags>
|
||||
<Tag icon="LockClosed">Pro plan</Tag>
|
||||
<Tag icon="LockClosed">Business</Tag>
|
||||
</Tags>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -30,8 +30,8 @@
|
|||
$: hasError = userData.find(x => x.error != null)
|
||||
|
||||
$: userCount = $licensing.userCount + userData.length
|
||||
$: willReach = licensing.willReachUserLimit(userCount)
|
||||
$: willExceed = licensing.willExceedUserLimit(userCount)
|
||||
$: reached = licensing.usersLimitReached(userCount)
|
||||
$: exceeded = licensing.usersLimitExceeded(userCount)
|
||||
|
||||
function removeInput(idx) {
|
||||
userData = userData.filter((e, i) => i !== idx)
|
||||
|
@ -87,7 +87,7 @@
|
|||
confirmDisabled={disabled}
|
||||
cancelText="Cancel"
|
||||
showCloseIcon={false}
|
||||
disabled={hasError || !userData.length || willExceed}
|
||||
disabled={hasError || !userData.length || exceeded}
|
||||
>
|
||||
<Layout noPadding gap="XS">
|
||||
<Label>Email address</Label>
|
||||
|
@ -118,7 +118,7 @@
|
|||
</div>
|
||||
{/each}
|
||||
|
||||
{#if willReach}
|
||||
{#if reached}
|
||||
<div class="user-notification">
|
||||
<Icon name="Info" />
|
||||
<span>
|
||||
|
|
|
@ -25,10 +25,10 @@
|
|||
$: invalidEmails = []
|
||||
|
||||
$: userCount = $licensing.userCount + userEmails.length
|
||||
$: willExceed = userCount > $licensing.userLimit
|
||||
$: exceed = licensing.usersLimitExceeded(userCount)
|
||||
|
||||
$: importDisabled =
|
||||
!userEmails.length || !validEmails(userEmails) || !usersRole || willExceed
|
||||
!userEmails.length || !validEmails(userEmails) || !usersRole || exceed
|
||||
|
||||
const validEmails = userEmails => {
|
||||
if ($admin.cloud && userEmails.length > MAX_USERS_UPLOAD_LIMIT) {
|
||||
|
@ -93,7 +93,7 @@
|
|||
</label>
|
||||
</div>
|
||||
|
||||
{#if willExceed}
|
||||
{#if exceed}
|
||||
<div class="user-notification">
|
||||
<Icon name="Info" />
|
||||
{capitalise($licensing.license.plan.type)} plan is limited to {$licensing.userLimit}
|
||||
|
|
|
@ -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 = []
|
||||
$: 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 => {
|
||||
fetch.update({
|
||||
|
@ -144,6 +172,7 @@
|
|||
}))
|
||||
try {
|
||||
inviteUsersResponse = await users.invite(payload)
|
||||
pendingInvites = await users.getInvites()
|
||||
inviteConfirmationModal.show()
|
||||
} catch (error) {
|
||||
notifications.error("Error inviting user")
|
||||
|
@ -232,12 +261,13 @@
|
|||
try {
|
||||
await groups.actions.init()
|
||||
groupsLoaded = true
|
||||
|
||||
pendingInvites = await users.getInvites()
|
||||
invitesLoaded = true
|
||||
} catch (error) {
|
||||
notifications.error("Error fetching user group data")
|
||||
}
|
||||
})
|
||||
|
||||
let staticUserLimit = $licensing.license.quotas.usage.static.users.value
|
||||
</script>
|
||||
|
||||
<Layout noPadding gap="M">
|
||||
|
@ -246,7 +276,7 @@
|
|||
<Body>Add users and control who gets access to your published apps</Body>
|
||||
</Layout>
|
||||
<Divider />
|
||||
{#if $licensing.warnUserLimit}
|
||||
{#if $licensing.errUserLimit}
|
||||
<InlineAlert
|
||||
type="error"
|
||||
onConfirm={() => {
|
||||
|
@ -258,13 +288,9 @@
|
|||
}}
|
||||
buttonText={isOwner ? "Upgrade" : "View plans"}
|
||||
cta
|
||||
header={`Users will soon be limited to ${staticUserLimit}`}
|
||||
message={`Our free plan is going to be limited to ${staticUserLimit} users in ${$licensing.userLimitDays}.
|
||||
|
||||
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.
|
||||
`}
|
||||
header="Account de-activated"
|
||||
message="Due to the free plan user limit being exceeded, your account has been de-activated.
|
||||
Upgrade your plan to re-activate your account."
|
||||
/>
|
||||
{/if}
|
||||
<div class="controls">
|
||||
|
@ -324,6 +350,15 @@
|
|||
goToNextPage={fetch.nextPage}
|
||||
/>
|
||||
</div>
|
||||
<Table
|
||||
schema={pendingSchema}
|
||||
data={parsedInvites}
|
||||
allowEditColumns={false}
|
||||
allowEditRows={false}
|
||||
{customRenderers}
|
||||
loading={!invitesLoaded}
|
||||
allowClickRows={false}
|
||||
/>
|
||||
</Layout>
|
||||
|
||||
<Modal bind:this={createUserModal}>
|
||||
|
|
|
@ -25,6 +25,8 @@ export function createDatasourcesStore() {
|
|||
store.update(state => ({
|
||||
...state,
|
||||
selectedDatasourceId: id,
|
||||
// Remove any possible schema error
|
||||
schemaError: null,
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ export function createGroupsStore() {
|
|||
// on the backend anyway
|
||||
if (get(licensing).groupsEnabled) {
|
||||
const groups = await API.getGroups()
|
||||
store.set(groups)
|
||||
store.set(groups.data)
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import { auth, admin } from "stores/portal"
|
|||
import { Constants } from "@budibase/frontend-core"
|
||||
import { StripeStatus } from "components/portal/licensing/constants"
|
||||
import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags"
|
||||
import dayjs from "dayjs"
|
||||
import { PlanModel } from "@budibase/types"
|
||||
|
||||
const UNLIMITED = -1
|
||||
|
||||
|
@ -12,6 +12,7 @@ export const createLicensingStore = () => {
|
|||
const DEFAULT = {
|
||||
// navigation
|
||||
goToUpgradePage: () => {},
|
||||
goToPricingPage: () => {},
|
||||
// the top level license
|
||||
license: undefined,
|
||||
isFreePlan: true,
|
||||
|
@ -37,29 +38,37 @@ export const createLicensingStore = () => {
|
|||
// user limits
|
||||
userCount: undefined,
|
||||
userLimit: undefined,
|
||||
userLimitDays: undefined,
|
||||
userLimitReached: false,
|
||||
warnUserLimit: false,
|
||||
errUserLimit: false,
|
||||
}
|
||||
|
||||
const oneDayInMilliseconds = 86400000
|
||||
|
||||
const store = writable(DEFAULT)
|
||||
|
||||
function willReachUserLimit(userCount, userLimit) {
|
||||
function usersLimitReached(userCount, userLimit) {
|
||||
if (userLimit === UNLIMITED) {
|
||||
return false
|
||||
}
|
||||
return userCount >= userLimit
|
||||
}
|
||||
|
||||
function willExceedUserLimit(userCount, userLimit) {
|
||||
function usersLimitExceeded(userCount, userLimit) {
|
||||
if (userLimit === UNLIMITED) {
|
||||
return false
|
||||
}
|
||||
return userCount > userLimit
|
||||
}
|
||||
|
||||
async function isCloud() {
|
||||
let adminStore = get(admin)
|
||||
if (!adminStore.loaded) {
|
||||
await admin.init()
|
||||
adminStore = get(admin)
|
||||
}
|
||||
return adminStore.cloud
|
||||
}
|
||||
|
||||
const actions = {
|
||||
init: async () => {
|
||||
actions.setNavigation()
|
||||
|
@ -71,10 +80,14 @@ export const createLicensingStore = () => {
|
|||
const goToUpgradePage = () => {
|
||||
window.location.href = upgradeUrl
|
||||
}
|
||||
const goToPricingPage = () => {
|
||||
window.open("https://budibase.com/pricing/", "_blank")
|
||||
}
|
||||
store.update(state => {
|
||||
return {
|
||||
...state,
|
||||
goToUpgradePage,
|
||||
goToPricingPage,
|
||||
}
|
||||
})
|
||||
},
|
||||
|
@ -128,15 +141,15 @@ export const createLicensingStore = () => {
|
|||
quotaUsage,
|
||||
}
|
||||
})
|
||||
actions.setUsageMetrics()
|
||||
await actions.setUsageMetrics()
|
||||
},
|
||||
willReachUserLimit: userCount => {
|
||||
return willReachUserLimit(userCount, get(store).userLimit)
|
||||
usersLimitReached: userCount => {
|
||||
return usersLimitReached(userCount, get(store).userLimit)
|
||||
},
|
||||
willExceedUserLimit(userCount) {
|
||||
return willExceedUserLimit(userCount, get(store).userLimit)
|
||||
usersLimitExceeded(userCount) {
|
||||
return usersLimitExceeded(userCount, get(store).userLimit)
|
||||
},
|
||||
setUsageMetrics: () => {
|
||||
setUsageMetrics: async () => {
|
||||
if (isEnabled(TENANT_FEATURE_FLAGS.LICENSING)) {
|
||||
const usage = get(store).quotaUsage
|
||||
const license = get(auth).user.license
|
||||
|
@ -198,11 +211,13 @@ export const createLicensingStore = () => {
|
|||
const userQuota = license.quotas.usage.static.users
|
||||
const userLimit = userQuota?.value
|
||||
const userCount = usage.usageQuota.users
|
||||
const userLimitReached = willReachUserLimit(userCount, userLimit)
|
||||
const userLimitExceeded = willExceedUserLimit(userCount, userLimit)
|
||||
const days = dayjs(userQuota?.startDate).diff(dayjs(), "day")
|
||||
const userLimitDays = days > 1 ? `${days} days` : "1 day"
|
||||
const warnUserLimit = userQuota?.startDate && userLimitExceeded
|
||||
const userLimitReached = usersLimitReached(userCount, userLimit)
|
||||
const userLimitExceeded = usersLimitExceeded(userCount, userLimit)
|
||||
const isCloudAccount = await isCloud()
|
||||
const errUserLimit =
|
||||
isCloudAccount &&
|
||||
license.plan.model === PlanModel.PER_USER &&
|
||||
userLimitExceeded
|
||||
|
||||
store.update(state => {
|
||||
return {
|
||||
|
@ -217,9 +232,8 @@ export const createLicensingStore = () => {
|
|||
// user limits
|
||||
userCount,
|
||||
userLimit,
|
||||
userLimitDays,
|
||||
userLimitReached,
|
||||
warnUserLimit,
|
||||
errUserLimit,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ export function createUsersStore() {
|
|||
inviteCode,
|
||||
password,
|
||||
firstName,
|
||||
lastName,
|
||||
lastName: !lastName?.trim() ? undefined : lastName,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -114,11 +114,13 @@ export function createUsersStore() {
|
|||
const getUserRole = ({ admin, builder }) =>
|
||||
admin?.global ? "admin" : builder?.global ? "developer" : "appUser"
|
||||
|
||||
const refreshUsage = fn => async args => {
|
||||
const response = await fn(args)
|
||||
await licensing.setQuotaUsage()
|
||||
return response
|
||||
}
|
||||
const refreshUsage =
|
||||
fn =>
|
||||
async (...args) => {
|
||||
const response = await fn(...args)
|
||||
await licensing.setQuotaUsage()
|
||||
return response
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
|
@ -133,7 +135,7 @@ export function createUsersStore() {
|
|||
updateInvite,
|
||||
getUserCountByApp,
|
||||
// any operation that adds or deletes users
|
||||
acceptInvite: refreshUsage(acceptInvite),
|
||||
acceptInvite,
|
||||
create: refreshUsage(create),
|
||||
save: refreshUsage(save),
|
||||
bulkDelete: refreshUsage(bulkDelete),
|
||||
|
|
|
@ -4,18 +4,9 @@
|
|||
"composite": true,
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@budibase/types": ["../types/src"],
|
||||
"@budibase/backend-core": ["../backend-core/src"],
|
||||
"@budibase/backend-core/*": ["../backend-core/*.js"]
|
||||
}
|
||||
"baseUrl": "."
|
||||
},
|
||||
"ts-node": {
|
||||
"require": ["tsconfig-paths/register"]
|
||||
},
|
||||
"references": [
|
||||
{ "path": "../types" },
|
||||
{ "path": "../backend-core" },
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"name": "@budibase/cli",
|
||||
"version": "2.5.6-alpha.30",
|
||||
"version": "0.0.1",
|
||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||
"main": "dist/index.js",
|
||||
"main": "dist/src/index.js",
|
||||
"bin": {
|
||||
"budi": "dist/index.js"
|
||||
"budi": "dist/src/index.js"
|
||||
},
|
||||
"author": "Budibase",
|
||||
"license": "GPL-3.0",
|
||||
|
@ -29,14 +29,14 @@
|
|||
"outputPath": "build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/backend-core": "2.5.6-alpha.30",
|
||||
"@budibase/string-templates": "2.5.6-alpha.30",
|
||||
"@budibase/types": "2.5.6-alpha.30",
|
||||
"@budibase/backend-core": "0.0.1",
|
||||
"@budibase/string-templates": "0.0.1",
|
||||
"@budibase/types": "0.0.1",
|
||||
"axios": "0.21.2",
|
||||
"chalk": "4.1.0",
|
||||
"cli-progress": "3.11.2",
|
||||
"commander": "7.1.0",
|
||||
"docker-compose": "0.23.12",
|
||||
"docker-compose": "0.24.0",
|
||||
"dotenv": "16.0.1",
|
||||
"download": "8.0.0",
|
||||
"find-free-port": "^2.0.0",
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
#!/usr/bin/env node
|
||||
import { logging } from "@budibase/backend-core"
|
||||
logging.disableLogger()
|
||||
process.env.DISABLE_PINO_LOGGER = "1"
|
||||
import "./prebuilds"
|
||||
import "./environment"
|
||||
import { env } from "@budibase/backend-core"
|
||||
import { getCommands } from "./options"
|
||||
import { Command } from "commander"
|
||||
import { getHelpDescription } from "./utils"
|
||||
import { version } from "../package.json"
|
||||
|
||||
// add hosting config
|
||||
async function init() {
|
||||
const program = new Command()
|
||||
.addHelpCommand("help", getHelpDescription("Help with Budibase commands."))
|
||||
.helpOption(false)
|
||||
.version(env.VERSION)
|
||||
.version(version)
|
||||
// add commands
|
||||
for (let command of getCommands()) {
|
||||
command.configure(program)
|
||||
|
|
|
@ -13,7 +13,7 @@ if (!process.argv[0].includes("node")) {
|
|||
}
|
||||
|
||||
function checkForBinaries() {
|
||||
const readDir = join(__filename, "..", "..", PREBUILDS, ARCH)
|
||||
const readDir = join(__filename, "..", "..", "..", PREBUILDS, ARCH)
|
||||
if (fs.existsSync(PREBUILD_DIR) || !fs.existsSync(readDir)) {
|
||||
return
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue