merge
This commit is contained in:
commit
bbab2a9adc
|
@ -22,42 +22,45 @@ jobs:
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Use Node.js 14.x
|
- name: Use Node.js 14.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 14.x
|
||||||
|
cache: "yarn"
|
||||||
- run: yarn
|
- run: yarn
|
||||||
- run: yarn lint
|
- run: yarn lint
|
||||||
|
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
- name: Use Node.js 14.x
|
- name: Use Node.js 14.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 14.x
|
||||||
- name: Install Pro
|
cache: "yarn"
|
||||||
run: yarn install:pro $BRANCH $BASE_BRANCH
|
|
||||||
- run: yarn
|
- run: yarn
|
||||||
- run: yarn bootstrap
|
|
||||||
- run: yarn build
|
- run: yarn build
|
||||||
|
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
- name: Use Node.js 14.x
|
- name: Use Node.js 14.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 14.x
|
||||||
- name: Install Pro
|
cache: "yarn"
|
||||||
run: yarn install:pro $BRANCH $BASE_BRANCH
|
|
||||||
- run: yarn
|
- run: yarn
|
||||||
- run: yarn bootstrap
|
- run: yarn build --scope=@budibase/types --scope=@budibase/shared-core --scope=@budibase/string-templates
|
||||||
- run: yarn build
|
- run: yarn test --ignore=@budibase/pro
|
||||||
- run: yarn test
|
|
||||||
- uses: codecov/codecov-action@v3
|
- uses: codecov/codecov-action@v3
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
||||||
|
@ -67,32 +70,58 @@ jobs:
|
||||||
test-pro:
|
test-pro:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
- name: Use Node.js 14.x
|
- name: Use Node.js 14.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 14.x
|
||||||
- name: Install Pro
|
cache: "yarn"
|
||||||
run: yarn install:pro $BRANCH $BASE_BRANCH
|
|
||||||
- run: yarn
|
- run: yarn
|
||||||
- run: yarn bootstrap
|
- run: yarn test --scope=@budibase/pro
|
||||||
- run: yarn test:pro
|
|
||||||
|
|
||||||
integration-test:
|
integration-test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
- name: Use Node.js 14.x
|
- name: Use Node.js 14.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 14.x
|
||||||
- name: Install Pro
|
cache: "yarn"
|
||||||
run: yarn install:pro $BRANCH $BASE_BRANCH
|
- run: yarn
|
||||||
- run: yarn && yarn bootstrap && yarn build
|
- run: yarn build
|
||||||
- run: |
|
- name: Run tests
|
||||||
|
run: |
|
||||||
cd qa-core
|
cd qa-core
|
||||||
yarn setup
|
yarn setup
|
||||||
yarn test:ci
|
yarn test:ci
|
||||||
env:
|
env:
|
||||||
BB_ADMIN_USER_EMAIL: admin
|
BB_ADMIN_USER_EMAIL: admin
|
||||||
BB_ADMIN_USER_PASSWORD: admin
|
BB_ADMIN_USER_PASSWORD: admin
|
||||||
|
|
||||||
|
check-pro-submodule:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Check submodule
|
||||||
|
run: |
|
||||||
|
cd packages/pro
|
||||||
|
git fetch
|
||||||
|
if ! git merge-base --is-ancestor $(git log -n 1 --pretty=format:%H) origin/develop; then
|
||||||
|
echo "Current commit has not been merged to develop"
|
||||||
|
echo "Refer to the pro repo to merge your changes: https://github.com/Budibase/budibase-pro/blob/develop/docs/getting_started.md"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "All good, the submodule had been merged!"
|
||||||
|
fi
|
||||||
|
|
|
@ -3,18 +3,8 @@ concurrency: release-prerelease
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
tags:
|
||||||
- develop
|
- v*-alpha.*
|
||||||
paths:
|
|
||||||
- '.aws/**'
|
|
||||||
- '.github/**'
|
|
||||||
- 'charts/**'
|
|
||||||
- 'packages/**'
|
|
||||||
- 'scripts/**'
|
|
||||||
- 'package.json'
|
|
||||||
- 'yarn.lock'
|
|
||||||
- 'package.json'
|
|
||||||
- 'yarn.lock'
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
@ -30,24 +20,39 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Fail if branch is not develop
|
|
||||||
if: github.ref != 'refs/heads/develop'
|
|
||||||
run: |
|
|
||||||
echo "Ref is not develop, you must run this job from develop."
|
|
||||||
exit 1
|
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Fail if tag is not develop
|
||||||
|
run: |
|
||||||
|
if ! git merge-base --is-ancestor ${{ github.sha }} origin/develop; then
|
||||||
|
echo "Tag is not in develop"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- uses: actions/setup-node@v1
|
- uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 14.x
|
||||||
|
|
||||||
- name: Install Pro
|
|
||||||
run: yarn install:pro develop
|
|
||||||
|
|
||||||
- run: yarn
|
- run: yarn
|
||||||
- run: yarn bootstrap
|
- name: Update versions
|
||||||
|
run: |
|
||||||
|
version=$(cat lerna.json \
|
||||||
|
| grep version \
|
||||||
|
| head -1 \
|
||||||
|
| awk -F: '{gsub(/"/,"",$2);gsub(/[[:space:]]*/,"",$2); print $2}' \
|
||||||
|
| sed 's/[",]//g')
|
||||||
|
echo "Setting version $version"
|
||||||
|
yarn lerna exec "yarn version --no-git-tag-version --new-version=$version"
|
||||||
|
echo "Updating dependencies"
|
||||||
|
node scripts/syncLocalDependencies.js $version
|
||||||
|
echo "Syncing yarn workspace"
|
||||||
|
yarn
|
||||||
- run: yarn build
|
- run: yarn build
|
||||||
- run: yarn build:sdk
|
- run: yarn build:sdk
|
||||||
# - run: yarn test
|
|
||||||
|
|
||||||
- name: Publish budibase packages to NPM
|
- name: Publish budibase packages to NPM
|
||||||
env:
|
env:
|
||||||
|
@ -56,6 +61,8 @@ jobs:
|
||||||
# setup the username and email.
|
# setup the username and email.
|
||||||
git config --global user.name "Budibase Staging Release Bot"
|
git config --global user.name "Budibase Staging Release Bot"
|
||||||
git config --global user.email "<>"
|
git config --global user.email "<>"
|
||||||
|
git submodule foreach git commit -a -m 'Release process'
|
||||||
|
git commit -a -m 'Release process'
|
||||||
echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc
|
echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc
|
||||||
yarn release:develop
|
yarn release:develop
|
||||||
|
|
||||||
|
@ -84,7 +91,7 @@ jobs:
|
||||||
git config user.name "Budibase Helm Bot"
|
git config user.name "Budibase Helm Bot"
|
||||||
git config user.email "<>"
|
git config user.email "<>"
|
||||||
git reset --hard
|
git reset --hard
|
||||||
git pull
|
git fetch
|
||||||
mkdir sync
|
mkdir sync
|
||||||
echo "Packaging chart to sync dir"
|
echo "Packaging chart to sync dir"
|
||||||
helm package charts/budibase --version 0.0.0-develop --app-version develop --destination sync
|
helm package charts/budibase --version 0.0.0-develop --app-version develop --destination sync
|
||||||
|
|
|
@ -3,29 +3,16 @@ concurrency: release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
tags:
|
||||||
- master
|
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||||
paths:
|
# Exclude all pre-releases
|
||||||
- '.aws/**'
|
- "!v*[0-9]+.[0-9]+.[0-9]+-*"
|
||||||
- '.github/**'
|
|
||||||
- 'charts/**'
|
|
||||||
- 'packages/**'
|
|
||||||
- 'scripts/**'
|
|
||||||
- 'package.json'
|
|
||||||
- 'yarn.lock'
|
|
||||||
- 'package.json'
|
|
||||||
- 'yarn.lock'
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
versioning:
|
tags:
|
||||||
type: choice
|
description: "Release tag"
|
||||||
description: "Versioning type: patch, minor, major"
|
|
||||||
default: patch
|
|
||||||
options:
|
|
||||||
- patch
|
|
||||||
- minor
|
|
||||||
- major
|
|
||||||
required: true
|
required: true
|
||||||
|
type: boolean
|
||||||
|
|
||||||
env:
|
env:
|
||||||
# Posthog token used by ui at build time
|
# Posthog token used by ui at build time
|
||||||
|
@ -38,21 +25,37 @@ jobs:
|
||||||
release-images:
|
release-images:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Fail if branch is not master
|
- name: Fail if branch is not master
|
||||||
if: github.ref != 'refs/heads/master'
|
if: github.ref != 'refs/heads/master'
|
||||||
run: |
|
run: |
|
||||||
echo "Ref is not master, you must run this job from master."
|
echo "Ref is not master, you must run this job from master."
|
||||||
exit 1
|
// Change to "exit 1" when merged. Left to 0 to not fail all the pipelines and not to cause noise
|
||||||
- uses: actions/checkout@v2
|
exit 0
|
||||||
|
|
||||||
- uses: actions/setup-node@v1
|
- uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 14.x
|
||||||
|
|
||||||
- name: Install Pro
|
|
||||||
run: yarn install:pro master
|
|
||||||
|
|
||||||
- run: yarn
|
- run: yarn
|
||||||
- run: yarn bootstrap
|
- name: Update versions
|
||||||
|
run: |
|
||||||
|
version=$(cat lerna.json \
|
||||||
|
| grep version \
|
||||||
|
| head -1 \
|
||||||
|
| awk -F: '{gsub(/"/,"",$2);gsub(/[[:space:]]*/,"",$2); print $2}' \
|
||||||
|
| sed 's/[",]//g')
|
||||||
|
echo "Setting version $version"
|
||||||
|
yarn lerna exec "yarn version --no-git-tag-version --new-version=$version"
|
||||||
|
echo "Updating dependencies"
|
||||||
|
node scripts/syncLocalDependencies.js $version
|
||||||
|
echo "Syncing yarn workspace"
|
||||||
|
yarn
|
||||||
- run: yarn lint
|
- run: yarn lint
|
||||||
- run: yarn build
|
- run: yarn build
|
||||||
- run: yarn build:sdk
|
- run: yarn build:sdk
|
||||||
|
@ -65,10 +68,12 @@ jobs:
|
||||||
# setup the username and email. I tend to use 'GitHub Actions Bot' with no email by default
|
# setup the username and email. I tend to use 'GitHub Actions Bot' with no email by default
|
||||||
git config --global user.name "Budibase Release Bot"
|
git config --global user.name "Budibase Release Bot"
|
||||||
git config --global user.email "<>"
|
git config --global user.email "<>"
|
||||||
|
git submodule foreach git commit -a -m 'Release process'
|
||||||
|
git commit -a -m 'Release process'
|
||||||
echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc
|
echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc
|
||||||
yarn release
|
yarn release
|
||||||
|
|
||||||
- name: 'Get Previous tag'
|
- name: "Get Previous tag"
|
||||||
id: previoustag
|
id: previoustag
|
||||||
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
||||||
|
|
||||||
|
@ -103,7 +108,7 @@ jobs:
|
||||||
git config user.name "Budibase Helm Bot"
|
git config user.name "Budibase Helm Bot"
|
||||||
git config user.email "<>"
|
git config user.email "<>"
|
||||||
git reset --hard
|
git reset --hard
|
||||||
git pull
|
git fetch
|
||||||
mkdir sync
|
mkdir sync
|
||||||
echo "Packaging chart to sync dir"
|
echo "Packaging chart to sync dir"
|
||||||
helm package charts/budibase --version 0.0.0-master --app-version v"$RELEASE_VERSION" --destination sync
|
helm package charts/budibase --version 0.0.0-master --app-version v"$RELEASE_VERSION" --destination sync
|
||||||
|
|
|
@ -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
|
## Dev Environment on Debian 11
|
||||||
|
|
||||||
### Install NVM & Node 14
|
### Install NVM & Node 14
|
||||||
|
|
||||||
NVM documentation: https://github.com/nvm-sh/nvm#installing-and-updating
|
NVM documentation: https://github.com/nvm-sh/nvm#installing-and-updating
|
||||||
|
|
||||||
Install NVM
|
Install NVM
|
||||||
|
|
||||||
```
|
```
|
||||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
|
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
Install Node 14
|
Install Node 14
|
||||||
|
|
||||||
```
|
```
|
||||||
nvm install 14
|
nvm install 14
|
||||||
```
|
```
|
||||||
|
@ -17,13 +21,16 @@ nvm install 14
|
||||||
```
|
```
|
||||||
npm install -g yarn jest lerna
|
npm install -g yarn jest lerna
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install Docker and Docker Compose
|
### Install Docker and Docker Compose
|
||||||
|
|
||||||
```
|
```
|
||||||
apt install docker.io
|
apt install docker.io
|
||||||
pip3 install docker-compose
|
pip3 install docker-compose
|
||||||
```
|
```
|
||||||
|
|
||||||
### Clone the repo
|
### Clone the repo
|
||||||
|
|
||||||
```
|
```
|
||||||
git clone https://github.com/Budibase/budibase.git
|
git clone https://github.com/Budibase/budibase.git
|
||||||
```
|
```
|
||||||
|
@ -44,10 +51,13 @@ This setup process was tested on Debian 11 (bullseye) with version numbers show
|
||||||
cd budibase
|
cd budibase
|
||||||
yarn setup
|
yarn setup
|
||||||
```
|
```
|
||||||
|
|
||||||
The yarn setup command runs several build steps i.e.
|
The yarn setup command runs several build steps i.e.
|
||||||
|
|
||||||
```
|
```
|
||||||
node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev
|
node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
So this command will actually run the application in dev mode. It creates .env files under `./packages/server` and `./packages/worker` and runs docker containers for each service via docker-compose.
|
So this command will actually run the application in dev mode. It creates .env files under `./packages/server` and `./packages/worker` and runs docker containers for each service via docker-compose.
|
||||||
|
|
||||||
The dev version will be available on port 10000 i.e.
|
The dev version will be available on port 10000 i.e.
|
||||||
|
@ -55,6 +65,7 @@ The dev version will be available on port 10000 i.e.
|
||||||
http://127.0.0.1:10000/builder/admin
|
http://127.0.0.1:10000/builder/admin
|
||||||
|
|
||||||
### File descriptor issues with Vite and Chrome in Linux
|
### File descriptor issues with Vite and Chrome in Linux
|
||||||
|
|
||||||
If your dev environment stalls forever, with some network requests stuck in flight, it's likely that Chrome is trying to open more file descriptors than your system allows.
|
If your dev environment stalls forever, with some network requests stuck in flight, it's likely that Chrome is trying to open more file descriptors than your system allows.
|
||||||
To fix this, apply the following tweaks.
|
To fix this, apply the following tweaks.
|
||||||
|
|
||||||
|
|
|
@ -8,10 +8,10 @@ Install instructions [here](https://brew.sh/)
|
||||||
`eval $(/opt/homebrew/bin/brew shellenv)` line to your `.zshrc`. This will make your zsh to find the apps you install
|
`eval $(/opt/homebrew/bin/brew shellenv)` line to your `.zshrc`. This will make your zsh to find the apps you install
|
||||||
through brew.
|
through brew.
|
||||||
|
|
||||||
|
|
||||||
### Install Node
|
### Install Node
|
||||||
|
|
||||||
Budibase requires a recent version of node 14:
|
Budibase requires a recent version of node 14:
|
||||||
|
|
||||||
```
|
```
|
||||||
brew install node npm
|
brew install node npm
|
||||||
node -v
|
node -v
|
||||||
|
@ -22,12 +22,15 @@ node -v
|
||||||
```
|
```
|
||||||
npm install -g yarn jest lerna
|
npm install -g yarn jest lerna
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install Docker and Docker Compose
|
### Install Docker and Docker Compose
|
||||||
|
|
||||||
```
|
```
|
||||||
brew install docker docker-compose
|
brew install docker docker-compose
|
||||||
```
|
```
|
||||||
|
|
||||||
### Clone the repo
|
### Clone the repo
|
||||||
|
|
||||||
```
|
```
|
||||||
git clone https://github.com/Budibase/budibase.git
|
git clone https://github.com/Budibase/budibase.git
|
||||||
```
|
```
|
||||||
|
@ -48,10 +51,13 @@ This setup process was tested on Mac OSX 12 (Monterey) with version numbers show
|
||||||
cd budibase
|
cd budibase
|
||||||
yarn setup
|
yarn setup
|
||||||
```
|
```
|
||||||
|
|
||||||
The yarn setup command runs several build steps i.e.
|
The yarn setup command runs several build steps i.e.
|
||||||
|
|
||||||
```
|
```
|
||||||
node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev
|
node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
So this command will actually run the application in dev mode. It creates .env files under `./packages/server` and `./packages/worker` and runs docker containers for each service via docker-compose.
|
So this command will actually run the application in dev mode. It creates .env files under `./packages/server` and `./packages/worker` and runs docker containers for each service via docker-compose.
|
||||||
|
|
||||||
The dev version will be available on port 10000 i.e.
|
The dev version will be available on port 10000 i.e.
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
## Dev Environment on Windows 10/11 (WSL2)
|
## Dev Environment on Windows 10/11 (WSL2)
|
||||||
|
|
||||||
|
|
||||||
### Install WSL with Ubuntu LTS
|
### Install WSL with Ubuntu LTS
|
||||||
|
|
||||||
Enable WSL 2 on Windows 10/11 for docker support.
|
Enable WSL 2 on Windows 10/11 for docker support.
|
||||||
|
|
||||||
```
|
```
|
||||||
wsl --set-default-version 2
|
wsl --set-default-version 2
|
||||||
```
|
```
|
||||||
|
|
||||||
Install Ubuntu LTS.
|
Install Ubuntu LTS.
|
||||||
|
|
||||||
```
|
```
|
||||||
wsl --install Ubuntu
|
wsl --install Ubuntu
|
||||||
```
|
```
|
||||||
|
@ -16,6 +18,7 @@ Or follow the instruction here:
|
||||||
https://learn.microsoft.com/en-us/windows/wsl/install
|
https://learn.microsoft.com/en-us/windows/wsl/install
|
||||||
|
|
||||||
### Install Docker in windows
|
### Install Docker in windows
|
||||||
|
|
||||||
Download the installer from docker and install it.
|
Download the installer from docker and install it.
|
||||||
|
|
||||||
Check this url for more detailed instructions:
|
Check this url for more detailed instructions:
|
||||||
|
@ -24,18 +27,21 @@ https://docs.docker.com/desktop/install/windows-install/
|
||||||
You should follow the next steps from within the Ubuntu terminal.
|
You should follow the next steps from within the Ubuntu terminal.
|
||||||
|
|
||||||
### Install NVM & Node 14
|
### Install NVM & Node 14
|
||||||
|
|
||||||
NVM documentation: https://github.com/nvm-sh/nvm#installing-and-updating
|
NVM documentation: https://github.com/nvm-sh/nvm#installing-and-updating
|
||||||
|
|
||||||
Install NVM
|
Install NVM
|
||||||
|
|
||||||
```
|
```
|
||||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
|
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
Install Node 14
|
Install Node 14
|
||||||
|
|
||||||
```
|
```
|
||||||
nvm install 14
|
nvm install 14
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### Install npm requirements
|
### Install npm requirements
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -43,6 +49,7 @@ npm install -g yarn jest lerna
|
||||||
```
|
```
|
||||||
|
|
||||||
### Clone the repo
|
### Clone the repo
|
||||||
|
|
||||||
```
|
```
|
||||||
git clone https://github.com/Budibase/budibase.git
|
git clone https://github.com/Budibase/budibase.git
|
||||||
```
|
```
|
||||||
|
@ -63,10 +70,13 @@ This setup process was tested on Windows 11 with version numbers show below. You
|
||||||
cd budibase
|
cd budibase
|
||||||
yarn setup
|
yarn setup
|
||||||
```
|
```
|
||||||
|
|
||||||
The yarn setup command runs several build steps i.e.
|
The yarn setup command runs several build steps i.e.
|
||||||
|
|
||||||
```
|
```
|
||||||
node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev
|
node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
So this command will actually run the application in dev mode. It creates .env files under `./packages/server` and `./packages/worker` and runs docker containers for each service via docker-compose.
|
So this command will actually run the application in dev mode. It creates .env files under `./packages/server` and `./packages/worker` and runs docker containers for each service via docker-compose.
|
||||||
|
|
||||||
The dev version will be available on port 10000 i.e.
|
The dev version will be available on port 10000 i.e.
|
||||||
|
@ -74,6 +84,7 @@ The dev version will be available on port 10000 i.e.
|
||||||
http://127.0.0.1:10000/builder/admin
|
http://127.0.0.1:10000/builder/admin
|
||||||
|
|
||||||
### Working with the code
|
### Working with the code
|
||||||
|
|
||||||
Here are the instructions to work on the application from within Visual Studio Code (in Windows) through the WSL. All the commands and files are within the Ubuntu system and it should run as if you were working on a Linux machine.
|
Here are the instructions to work on the application from within Visual Studio Code (in Windows) through the WSL. All the commands and files are within the Ubuntu system and it should run as if you were working on a Linux machine.
|
||||||
|
|
||||||
https://code.visualstudio.com/docs/remote/wsl
|
https://code.visualstudio.com/docs/remote/wsl
|
||||||
|
|
|
@ -5,8 +5,11 @@ ENV COUCHDB_PASSWORD admin
|
||||||
EXPOSE 5984
|
EXPOSE 5984
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends software-properties-common wget unzip curl && \
|
RUN apt-get update && apt-get install -y --no-install-recommends software-properties-common wget unzip curl && \
|
||||||
apt-add-repository 'deb http://security.debian.org/debian-security stretch/updates main' && \
|
wget -qO - https://adoptopenjdk.jfrog.io/adoptopenjdk/api/gpg/key/public | apt-key add - && \
|
||||||
apt-get update && apt-get install -y --no-install-recommends openjdk-8-jre && \
|
apt-add-repository 'deb http://security.debian.org/debian-security bullseye-security/updates main' && \
|
||||||
|
apt-add-repository 'deb http://archive.debian.org/debian stretch-backports main' && \
|
||||||
|
apt-add-repository --yes https://adoptopenjdk.jfrog.io/adoptopenjdk/deb/ && \
|
||||||
|
apt-get update && apt-get install -y --no-install-recommends adoptopenjdk-8-hotspot && \
|
||||||
rm -rf /var/lib/apt/lists/
|
rm -rf /var/lib/apt/lists/
|
||||||
|
|
||||||
# setup clouseau
|
# setup clouseau
|
||||||
|
|
|
@ -55,7 +55,7 @@ http {
|
||||||
set $csp_style "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me https://maxcdn.bootstrapcdn.com";
|
set $csp_style "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me https://maxcdn.bootstrapcdn.com";
|
||||||
set $csp_object "object-src 'none'";
|
set $csp_object "object-src 'none'";
|
||||||
set $csp_base_uri "base-uri 'self'";
|
set $csp_base_uri "base-uri 'self'";
|
||||||
set $csp_connect "connect-src 'self' https://*.budibase.net https://api-iam.intercom.io https://api-iam.intercom.io https://api-ping.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io https://nexus-websocket-a.intercom.io https://nexus-websocket-b.intercom.io https://uploads.intercomcdn.com https://uploads.intercomusercontent.com https://*.s3.amazonaws.com https://*.s3.us-east-2.amazonaws.com https://*.s3.us-east-1.amazonaws.com https://*.s3.us-west-1.amazonaws.com https://*.s3.us-west-2.amazonaws.com https://*.s3.af-south-1.amazonaws.com https://*.s3.ap-east-1.amazonaws.com https://*.s3.ap-southeast-3.amazonaws.com https://*.s3.ap-south-1.amazonaws.com https://*.s3.ap-northeast-3.amazonaws.com https://*.s3.ap-northeast-2.amazonaws.com https://*.s3.ap-southeast-1.amazonaws.com https://*.s3.ap-southeast-2.amazonaws.com https://*.s3.ap-northeast-1.amazonaws.com https://*.s3.ca-central-1.amazonaws.com https://*.s3.cn-north-1.amazonaws.com https://*.s3.cn-northwest-1.amazonaws.com https://*.s3.eu-central-1.amazonaws.com https://*.s3.eu-west-1.amazonaws.com https://*.s3.eu-west-2.amazonaws.com https://*.s3.eu-south-1.amazonaws.com https://*.s3.eu-west-3.amazonaws.com https://*.s3.eu-north-1.amazonaws.com https://*.s3.sa-east-1.amazonaws.com https://*.s3.me-south-1.amazonaws.com https://*.s3.us-gov-east-1.amazonaws.com https://*.s3.us-gov-west-1.amazonaws.com https://api.github.com";
|
set $csp_connect "connect-src 'self' https://*.budibase.net https://api-iam.intercom.io https://api-iam.intercom.io https://api-ping.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io https://nexus-websocket-a.intercom.io https://nexus-websocket-b.intercom.io https://uploads.intercomcdn.com https://uploads.intercomusercontent.com https://*.amazonaws.com https://*.s3.amazonaws.com https://*.s3.us-east-2.amazonaws.com https://*.s3.us-east-1.amazonaws.com https://*.s3.us-west-1.amazonaws.com https://*.s3.us-west-2.amazonaws.com https://*.s3.af-south-1.amazonaws.com https://*.s3.ap-east-1.amazonaws.com https://*.s3.ap-southeast-3.amazonaws.com https://*.s3.ap-south-1.amazonaws.com https://*.s3.ap-northeast-3.amazonaws.com https://*.s3.ap-northeast-2.amazonaws.com https://*.s3.ap-southeast-1.amazonaws.com https://*.s3.ap-southeast-2.amazonaws.com https://*.s3.ap-northeast-1.amazonaws.com https://*.s3.ca-central-1.amazonaws.com https://*.s3.cn-north-1.amazonaws.com https://*.s3.cn-northwest-1.amazonaws.com https://*.s3.eu-central-1.amazonaws.com https://*.s3.eu-west-1.amazonaws.com https://*.s3.eu-west-2.amazonaws.com https://*.s3.eu-south-1.amazonaws.com https://*.s3.eu-west-3.amazonaws.com https://*.s3.eu-north-1.amazonaws.com https://*.s3.sa-east-1.amazonaws.com https://*.s3.me-south-1.amazonaws.com https://*.s3.us-gov-east-1.amazonaws.com https://*.s3.us-gov-west-1.amazonaws.com https://api.github.com";
|
||||||
set $csp_font "font-src 'self' data: https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me https://maxcdn.bootstrapcdn.com https://js.intercomcdn.com https://fonts.intercomcdn.com";
|
set $csp_font "font-src 'self' data: https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me https://maxcdn.bootstrapcdn.com https://js.intercomcdn.com https://fonts.intercomcdn.com";
|
||||||
set $csp_frame "frame-src 'self' https:";
|
set $csp_frame "frame-src 'self' https:";
|
||||||
set $csp_img "img-src http: https: data: blob:";
|
set $csp_img "img-src http: https: data: blob:";
|
||||||
|
@ -82,6 +82,12 @@ http {
|
||||||
set $couchdb ${COUCHDB_UPSTREAM_URL};
|
set $couchdb ${COUCHDB_UPSTREAM_URL};
|
||||||
set $watchtower ${WATCHTOWER_UPSTREAM_URL};
|
set $watchtower ${WATCHTOWER_UPSTREAM_URL};
|
||||||
|
|
||||||
|
location /health {
|
||||||
|
access_log off;
|
||||||
|
add_header 'Content-Type' 'application/json';
|
||||||
|
return 200 '{ "status": "OK" }';
|
||||||
|
}
|
||||||
|
|
||||||
location /app {
|
location /app {
|
||||||
proxy_pass $apps;
|
proxy_pass $apps;
|
||||||
}
|
}
|
||||||
|
@ -222,9 +228,9 @@ http {
|
||||||
rewrite ^/files/signed/(.*)$ /$1 break;
|
rewrite ^/files/signed/(.*)$ /$1 break;
|
||||||
}
|
}
|
||||||
|
|
||||||
client_header_timeout 60;
|
client_header_timeout 120;
|
||||||
client_body_timeout 60;
|
client_body_timeout 120;
|
||||||
keepalive_timeout 60;
|
keepalive_timeout 120;
|
||||||
|
|
||||||
# gzip
|
# gzip
|
||||||
gzip on;
|
gzip on;
|
||||||
|
|
|
@ -22,7 +22,7 @@ FROM budibase/couchdb
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ENV TARGETARCH $TARGETARCH
|
ENV TARGETARCH $TARGETARCH
|
||||||
#TARGETBUILD can be set to single (for single docker image) or aas (for azure app service)
|
#TARGETBUILD can be set to single (for single docker image) or aas (for azure app service)
|
||||||
# e.g. docker build --build-arg TARGETBUILD=aas ....
|
# e.g. docker build --build-arg TARGETBUILD=aas ....
|
||||||
ARG TARGETBUILD=single
|
ARG TARGETBUILD=single
|
||||||
ENV TARGETBUILD $TARGETBUILD
|
ENV TARGETBUILD $TARGETBUILD
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ COPY --from=build /worker /worker
|
||||||
# install base dependencies
|
# install base dependencies
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends software-properties-common nginx uuid-runtime redis-server && \
|
apt-get install -y --no-install-recommends software-properties-common nginx uuid-runtime redis-server && \
|
||||||
apt-add-repository 'deb http://security.debian.org/debian-security stretch/updates main' && \
|
apt-add-repository 'deb http://security.debian.org/debian-security bullseye-security/updates main' && \
|
||||||
apt-get update
|
apt-get update
|
||||||
|
|
||||||
# install other dependencies, nodejs, oracle requirements, jdk8, redis, nginx
|
# install other dependencies, nodejs, oracle requirements, jdk8, redis, nginx
|
||||||
|
|
18
lerna.json
18
lerna.json
|
@ -1,8 +1,22 @@
|
||||||
{
|
{
|
||||||
"version": "2.5.6-alpha.30",
|
"version": "2.6.19-alpha.4",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
|
"packages": [
|
||||||
|
"packages/backend-core",
|
||||||
|
"packages/bbui",
|
||||||
|
"packages/builder",
|
||||||
|
"packages/cli",
|
||||||
|
"packages/client",
|
||||||
|
"packages/frontend-core",
|
||||||
|
"packages/sdk",
|
||||||
|
"packages/server",
|
||||||
|
"packages/shared-core",
|
||||||
|
"packages/string-templates",
|
||||||
|
"packages/types",
|
||||||
|
"packages/worker",
|
||||||
|
"packages/pro/packages/pro"
|
||||||
|
],
|
||||||
"useWorkspaces": true,
|
"useWorkspaces": true,
|
||||||
"packages": ["packages/*"],
|
|
||||||
"command": {
|
"command": {
|
||||||
"publish": {
|
"publish": {
|
||||||
"ignoreChanges": [
|
"ignoreChanges": [
|
||||||
|
|
36
package.json
36
package.json
|
@ -8,7 +8,7 @@
|
||||||
"eslint": "^7.28.0",
|
"eslint": "^7.28.0",
|
||||||
"eslint-plugin-cypress": "^2.11.3",
|
"eslint-plugin-cypress": "^2.11.3",
|
||||||
"eslint-plugin-svelte3": "^3.2.0",
|
"eslint-plugin-svelte3": "^3.2.0",
|
||||||
"husky": "^7.0.1",
|
"husky": "^8.0.3",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"kill-port": "^1.6.1",
|
"kill-port": "^1.6.1",
|
||||||
"lerna": "^6.6.1",
|
"lerna": "^6.6.1",
|
||||||
|
@ -17,22 +17,22 @@
|
||||||
"prettier-plugin-svelte": "^2.3.0",
|
"prettier-plugin-svelte": "^2.3.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rollup-plugin-replace": "^2.2.0",
|
"rollup-plugin-replace": "^2.2.0",
|
||||||
|
"semver": "^7.5.0",
|
||||||
"svelte": "^3.38.2",
|
"svelte": "^3.38.2",
|
||||||
"typescript": "4.7.3"
|
"typescript": "4.7.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"setup": "node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev",
|
"preinstall": "node scripts/syncProPackage.js",
|
||||||
"bootstrap": "lerna link && ./scripts/link-dependencies.sh",
|
"setup": "git config submodule.recurse true && git submodule update && node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev",
|
||||||
|
"bootstrap": "./scripts/bootstrap.sh && lerna link && ./scripts/link-dependencies.sh",
|
||||||
"build": "lerna run --stream build",
|
"build": "lerna run --stream build",
|
||||||
"build:dev": "lerna run --stream prebuild && tsc --build --watch --preserveWatchOutput",
|
"build:dev": "lerna run --stream prebuild && yarn nx run-many --target=build --output-style=dynamic --watch --preserveWatchOutput",
|
||||||
"backend:bootstrap": "./scripts/scopeBackend.sh && yarn run bootstrap",
|
"backend:bootstrap": "./scripts/scopeBackend.sh && yarn run bootstrap",
|
||||||
"backend:build": "./scripts/scopeBackend.sh 'lerna run --stream build'",
|
"backend:build": "./scripts/scopeBackend.sh 'lerna run --stream build'",
|
||||||
"build:sdk": "lerna run --stream build:sdk",
|
"build:sdk": "lerna run --stream build:sdk",
|
||||||
"deps:circular": "madge packages/server/dist/index.js packages/worker/src/index.ts packages/backend-core/dist/src/index.js packages/cli/src/index.js --circular",
|
"deps:circular": "madge packages/server/dist/index.js packages/worker/src/index.ts packages/backend-core/dist/src/index.js packages/cli/src/index.js --circular",
|
||||||
"release": "lerna publish ${RELEASE_VERSION_TYPE:-patch} --yes --force-publish && yarn release:pro",
|
"release": "lerna publish ${RELEASE_VERSION_TYPE:-patch} --yes --force-publish --no-git-tag-version --no-push --no-git-reset",
|
||||||
"release:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop --exact && yarn release:pro:develop",
|
"release:develop": "lerna publish from-package --yes --force-publish --dist-tag develop --exact --no-git-tag-version --no-push --no-git-reset",
|
||||||
"release:pro": "bash scripts/pro/release.sh",
|
|
||||||
"release:pro:develop": "bash scripts/pro/release.sh develop",
|
|
||||||
"restore": "yarn run clean && yarn run bootstrap && yarn run build",
|
"restore": "yarn run clean && yarn run bootstrap && yarn run build",
|
||||||
"nuke": "yarn run nuke:packages && yarn run nuke:docker",
|
"nuke": "yarn run nuke:packages && yarn run nuke:docker",
|
||||||
"nuke:packages": "yarn run restore",
|
"nuke:packages": "yarn run restore",
|
||||||
|
@ -46,7 +46,6 @@
|
||||||
"dev:server": "yarn run kill-server && lerna run --stream --parallel dev:builder --concurrency 1 --scope @budibase/backend-core --scope @budibase/worker --scope @budibase/server",
|
"dev:server": "yarn run kill-server && lerna run --stream --parallel dev:builder --concurrency 1 --scope @budibase/backend-core --scope @budibase/worker --scope @budibase/server",
|
||||||
"dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream --parallel dev:built",
|
"dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream --parallel dev:built",
|
||||||
"test": "lerna run --stream test --stream",
|
"test": "lerna run --stream test --stream",
|
||||||
"test:pro": "bash scripts/pro/test.sh",
|
|
||||||
"lint:eslint": "eslint packages && eslint qa-core",
|
"lint:eslint": "eslint packages && eslint qa-core",
|
||||||
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --check \"qa-core/**/*.{js,ts,svelte}\"",
|
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --check \"qa-core/**/*.{js,ts,svelte}\"",
|
||||||
"lint": "yarn run lint:eslint && yarn run lint:prettier",
|
"lint": "yarn run lint:eslint && yarn run lint:prettier",
|
||||||
|
@ -82,12 +81,25 @@
|
||||||
"mode:account": "yarn mode:cloud && yarn env:account:enable",
|
"mode:account": "yarn mode:cloud && yarn env:account:enable",
|
||||||
"security:audit": "node scripts/audit.js",
|
"security:audit": "node scripts/audit.js",
|
||||||
"postinstall": "husky install",
|
"postinstall": "husky install",
|
||||||
"install:pro": "bash scripts/pro/install.sh",
|
"dep:clean": "yarn clean -y && yarn bootstrap",
|
||||||
"dep:clean": "yarn clean && yarn bootstrap"
|
"submodules:load": "git submodule init && git submodule update && yarn && yarn bootstrap",
|
||||||
|
"submodules:unload": "git submodule deinit --all && yarn && yarn bootstrap"
|
||||||
},
|
},
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/backend-core",
|
||||||
|
"packages/bbui",
|
||||||
|
"packages/builder",
|
||||||
|
"packages/cli",
|
||||||
|
"packages/client",
|
||||||
|
"packages/frontend-core",
|
||||||
|
"packages/sdk",
|
||||||
|
"packages/server",
|
||||||
|
"packages/shared-core",
|
||||||
|
"packages/string-templates",
|
||||||
|
"packages/types",
|
||||||
|
"packages/worker",
|
||||||
|
"packages/pro/packages/pro"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/backend-core",
|
"name": "@budibase/backend-core",
|
||||||
"version": "2.5.6-alpha.30",
|
"version": "0.0.1",
|
||||||
"description": "Budibase backend core libraries used in server and worker",
|
"description": "Budibase backend core libraries used in server and worker",
|
||||||
"main": "dist/src/index.js",
|
"main": "dist/src/index.js",
|
||||||
"types": "dist/src/index.d.ts",
|
"types": "dist/src/index.d.ts",
|
||||||
|
@ -15,8 +15,6 @@
|
||||||
"prebuild": "rimraf dist/",
|
"prebuild": "rimraf dist/",
|
||||||
"prepack": "cp package.json dist",
|
"prepack": "cp package.json dist",
|
||||||
"build": "tsc -p tsconfig.build.json",
|
"build": "tsc -p tsconfig.build.json",
|
||||||
"build:pro": "../../scripts/pro/build.sh",
|
|
||||||
"postbuild": "yarn run build:pro",
|
|
||||||
"build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput",
|
"build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput",
|
||||||
"test": "bash scripts/test.sh",
|
"test": "bash scripts/test.sh",
|
||||||
"test:watch": "jest --watchAll"
|
"test:watch": "jest --watchAll"
|
||||||
|
@ -24,7 +22,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/nano": "10.1.2",
|
"@budibase/nano": "10.1.2",
|
||||||
"@budibase/pouchdb-replication-stream": "1.2.10",
|
"@budibase/pouchdb-replication-stream": "1.2.10",
|
||||||
"@budibase/types": "2.5.6-alpha.30",
|
"@budibase/types": "0.0.1",
|
||||||
"@shopify/jest-koa-mocks": "5.0.1",
|
"@shopify/jest-koa-mocks": "5.0.1",
|
||||||
"@techpass/passport-openidconnect": "0.3.2",
|
"@techpass/passport-openidconnect": "0.3.2",
|
||||||
"aws-cloudfront-sign": "2.2.0",
|
"aws-cloudfront-sign": "2.2.0",
|
||||||
|
|
|
@ -47,7 +47,7 @@ async function put(
|
||||||
type: LockType.TRY_ONCE,
|
type: LockType.TRY_ONCE,
|
||||||
name: LockName.PERSIST_WRITETHROUGH,
|
name: LockName.PERSIST_WRITETHROUGH,
|
||||||
resource: key,
|
resource: key,
|
||||||
ttl: 1000,
|
ttl: 15000,
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
const writeDb = async (toWrite: any) => {
|
const writeDb = async (toWrite: any) => {
|
||||||
|
@ -71,6 +71,7 @@ async function put(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!lockResponse.executed) {
|
if (!lockResponse.executed) {
|
||||||
logWarn(`Ignoring redlock conflict in write-through cache`)
|
logWarn(`Ignoring redlock conflict in write-through cache`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ export enum ViewName {
|
||||||
AUTOMATION_LOGS = "automation_logs",
|
AUTOMATION_LOGS = "automation_logs",
|
||||||
ACCOUNT_BY_EMAIL = "account_by_email",
|
ACCOUNT_BY_EMAIL = "account_by_email",
|
||||||
PLATFORM_USERS_LOWERCASE = "platform_users_lowercase",
|
PLATFORM_USERS_LOWERCASE = "platform_users_lowercase",
|
||||||
USER_BY_GROUP = "by_group_user",
|
USER_BY_GROUP = "user_by_group",
|
||||||
APP_BACKUP_BY_TRIGGER = "by_trigger",
|
APP_BACKUP_BY_TRIGGER = "by_trigger",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -104,6 +104,22 @@ async function newContext(updates: ContextMap, task: any) {
|
||||||
return Context.run(context, task)
|
return Context.run(context, task)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function doInAutomationContext(params: {
|
||||||
|
appId: string
|
||||||
|
automationId: string
|
||||||
|
task: any
|
||||||
|
}): Promise<any> {
|
||||||
|
const tenantId = getTenantIDFromAppID(params.appId)
|
||||||
|
return newContext(
|
||||||
|
{
|
||||||
|
tenantId,
|
||||||
|
appId: params.appId,
|
||||||
|
automationId: params.automationId,
|
||||||
|
},
|
||||||
|
params.task
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export async function doInContext(appId: string, task: any): Promise<any> {
|
export async function doInContext(appId: string, task: any): Promise<any> {
|
||||||
const tenantId = getTenantIDFromAppID(appId)
|
const tenantId = getTenantIDFromAppID(appId)
|
||||||
return newContext(
|
return newContext(
|
||||||
|
@ -187,6 +203,11 @@ export function getTenantId(): string {
|
||||||
return tenantId
|
return tenantId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getAutomationId(): string | undefined {
|
||||||
|
const context = Context.get()
|
||||||
|
return context?.automationId
|
||||||
|
}
|
||||||
|
|
||||||
export function getAppId(): string | undefined {
|
export function getAppId(): string | undefined {
|
||||||
const context = Context.get()
|
const context = Context.get()
|
||||||
const foundId = context?.appId
|
const foundId = context?.appId
|
||||||
|
|
|
@ -7,4 +7,5 @@ export type ContextMap = {
|
||||||
identity?: IdentityContext
|
identity?: IdentityContext
|
||||||
environmentVariables?: Record<string, string>
|
environmentVariables?: Record<string, string>
|
||||||
isScim?: boolean
|
isScim?: boolean
|
||||||
|
automationId?: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import {
|
||||||
isDocument,
|
isDocument,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { getCouchInfo } from "./connections"
|
import { getCouchInfo } from "./connections"
|
||||||
import { directCouchCall } from "./utils"
|
import { directCouchUrlCall } from "./utils"
|
||||||
import { getPouchDB } from "./pouchDB"
|
import { getPouchDB } from "./pouchDB"
|
||||||
import { WriteStream, ReadStream } from "fs"
|
import { WriteStream, ReadStream } from "fs"
|
||||||
import { newid } from "../../docIds/newid"
|
import { newid } from "../../docIds/newid"
|
||||||
|
@ -46,6 +46,8 @@ export class DatabaseImpl implements Database {
|
||||||
private readonly instanceNano?: Nano.ServerScope
|
private readonly instanceNano?: Nano.ServerScope
|
||||||
private readonly pouchOpts: DatabaseOpts
|
private readonly pouchOpts: DatabaseOpts
|
||||||
|
|
||||||
|
private readonly couchInfo = getCouchInfo()
|
||||||
|
|
||||||
constructor(dbName?: string, opts?: DatabaseOpts, connection?: string) {
|
constructor(dbName?: string, opts?: DatabaseOpts, connection?: string) {
|
||||||
if (dbName == null) {
|
if (dbName == null) {
|
||||||
throw new Error("Database name cannot be undefined.")
|
throw new Error("Database name cannot be undefined.")
|
||||||
|
@ -53,8 +55,8 @@ export class DatabaseImpl implements Database {
|
||||||
this.name = dbName
|
this.name = dbName
|
||||||
this.pouchOpts = opts || {}
|
this.pouchOpts = opts || {}
|
||||||
if (connection) {
|
if (connection) {
|
||||||
const couchInfo = getCouchInfo(connection)
|
this.couchInfo = getCouchInfo(connection)
|
||||||
this.instanceNano = buildNano(couchInfo)
|
this.instanceNano = buildNano(this.couchInfo)
|
||||||
}
|
}
|
||||||
if (!DatabaseImpl.nano) {
|
if (!DatabaseImpl.nano) {
|
||||||
DatabaseImpl.init()
|
DatabaseImpl.init()
|
||||||
|
@ -67,7 +69,11 @@ export class DatabaseImpl implements Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
async exists() {
|
async exists() {
|
||||||
let response = await directCouchCall(`/${this.name}`, "HEAD")
|
const response = await directCouchUrlCall({
|
||||||
|
url: `${this.couchInfo.url}/${this.name}`,
|
||||||
|
method: "HEAD",
|
||||||
|
cookie: this.couchInfo.cookie,
|
||||||
|
})
|
||||||
return response.status === 200
|
return response.status === 200
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,21 +4,21 @@ export const getCouchInfo = (connection?: string) => {
|
||||||
const urlInfo = getUrlInfo(connection)
|
const urlInfo = getUrlInfo(connection)
|
||||||
let username
|
let username
|
||||||
let password
|
let password
|
||||||
if (env.COUCH_DB_USERNAME) {
|
if (urlInfo.auth?.username) {
|
||||||
// set from env
|
|
||||||
username = env.COUCH_DB_USERNAME
|
|
||||||
} else if (urlInfo.auth.username) {
|
|
||||||
// set from url
|
// set from url
|
||||||
username = urlInfo.auth.username
|
username = urlInfo.auth.username
|
||||||
|
} else if (env.COUCH_DB_USERNAME) {
|
||||||
|
// set from env
|
||||||
|
username = env.COUCH_DB_USERNAME
|
||||||
} else if (!env.isTest()) {
|
} else if (!env.isTest()) {
|
||||||
throw new Error("CouchDB username not set")
|
throw new Error("CouchDB username not set")
|
||||||
}
|
}
|
||||||
if (env.COUCH_DB_PASSWORD) {
|
if (urlInfo.auth?.password) {
|
||||||
// set from env
|
|
||||||
password = env.COUCH_DB_PASSWORD
|
|
||||||
} else if (urlInfo.auth.password) {
|
|
||||||
// set from url
|
// set from url
|
||||||
password = urlInfo.auth.password
|
password = urlInfo.auth.password
|
||||||
|
} else if (env.COUCH_DB_PASSWORD) {
|
||||||
|
// set from env
|
||||||
|
password = env.COUCH_DB_PASSWORD
|
||||||
} else if (!env.isTest()) {
|
} else if (!env.isTest()) {
|
||||||
throw new Error("CouchDB password not set")
|
throw new Error("CouchDB password not set")
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,20 @@ export async function directCouchCall(
|
||||||
) {
|
) {
|
||||||
let { url, cookie } = getCouchInfo()
|
let { url, cookie } = getCouchInfo()
|
||||||
const couchUrl = `${url}/${path}`
|
const couchUrl = `${url}/${path}`
|
||||||
|
return await directCouchUrlCall({ url: couchUrl, cookie, method, body })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function directCouchUrlCall({
|
||||||
|
url,
|
||||||
|
cookie,
|
||||||
|
method,
|
||||||
|
body,
|
||||||
|
}: {
|
||||||
|
url: string
|
||||||
|
cookie: string
|
||||||
|
method: string
|
||||||
|
body?: any
|
||||||
|
}) {
|
||||||
const params: any = {
|
const params: any = {
|
||||||
method: method,
|
method: method,
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -19,7 +33,7 @@ export async function directCouchCall(
|
||||||
params.body = JSON.stringify(body)
|
params.body = JSON.stringify(body)
|
||||||
params.headers["Content-Type"] = "application/json"
|
params.headers["Content-Type"] = "application/json"
|
||||||
}
|
}
|
||||||
return await fetch(checkSlashesInUrl(encodeURI(couchUrl)), params)
|
return await fetch(checkSlashesInUrl(encodeURI(url)), params)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function directCouchQuery(
|
export async function directCouchQuery(
|
||||||
|
|
|
@ -434,7 +434,7 @@ export class QueryBuilder<T> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (this.#query.empty) {
|
if (this.#query.empty) {
|
||||||
build(this.#query.empty, (key: string) => `!${key}:["" TO *]`)
|
build(this.#query.empty, (key: string) => `(*:* -${key}:["" TO *])`)
|
||||||
}
|
}
|
||||||
if (this.#query.notEmpty) {
|
if (this.#query.notEmpty) {
|
||||||
build(this.#query.notEmpty, (key: string) => `${key}:["" TO *]`)
|
build(this.#query.notEmpty, (key: string) => `${key}:["" TO *]`)
|
||||||
|
|
|
@ -69,10 +69,10 @@ function findVersion() {
|
||||||
try {
|
try {
|
||||||
const packageJsonFile = findFileInAncestors("package.json", process.cwd())
|
const packageJsonFile = findFileInAncestors("package.json", process.cwd())
|
||||||
const content = readFileSync(packageJsonFile!, "utf-8")
|
const content = readFileSync(packageJsonFile!, "utf-8")
|
||||||
const version = JSON.parse(content).version
|
return JSON.parse(content).version
|
||||||
return version
|
|
||||||
} catch {
|
} catch {
|
||||||
throw new Error("Cannot find a valid version in its package.json")
|
// throwing an error here is confusing/causes backend-core to be hard to import
|
||||||
|
return undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,7 +95,8 @@ const environment = {
|
||||||
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
|
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
|
||||||
SALT_ROUNDS: process.env.SALT_ROUNDS,
|
SALT_ROUNDS: process.env.SALT_ROUNDS,
|
||||||
REDIS_URL: process.env.REDIS_URL || "localhost:6379",
|
REDIS_URL: process.env.REDIS_URL || "localhost:6379",
|
||||||
REDIS_PASSWORD: process.env.REDIS_PASSWORD || "budibase",
|
REDIS_PASSWORD: process.env.REDIS_PASSWORD,
|
||||||
|
REDIS_CLUSTERED: process.env.REDIS_CLUSTERED,
|
||||||
MOCK_REDIS: process.env.MOCK_REDIS,
|
MOCK_REDIS: process.env.MOCK_REDIS,
|
||||||
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
|
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
|
||||||
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
|
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
|
||||||
|
@ -154,6 +155,7 @@ const environment = {
|
||||||
? process.env.ENABLE_SSO_MAINTENANCE_MODE
|
? process.env.ENABLE_SSO_MAINTENANCE_MODE
|
||||||
: false,
|
: false,
|
||||||
VERSION: findVersion(),
|
VERSION: findVersion(),
|
||||||
|
DISABLE_PINO_LOGGER: process.env.DISABLE_PINO_LOGGER,
|
||||||
_set(key: any, value: any) {
|
_set(key: any, value: any) {
|
||||||
process.env[key] = value
|
process.env[key] = value
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
|
@ -3,7 +3,6 @@ import {
|
||||||
Event,
|
Event,
|
||||||
LicenseActivatedEvent,
|
LicenseActivatedEvent,
|
||||||
LicensePlanChangedEvent,
|
LicensePlanChangedEvent,
|
||||||
LicenseTierChangedEvent,
|
|
||||||
PlanType,
|
PlanType,
|
||||||
Account,
|
Account,
|
||||||
LicensePortalOpenedEvent,
|
LicensePortalOpenedEvent,
|
||||||
|
@ -11,22 +10,23 @@ import {
|
||||||
LicenseCheckoutOpenedEvent,
|
LicenseCheckoutOpenedEvent,
|
||||||
LicensePaymentFailedEvent,
|
LicensePaymentFailedEvent,
|
||||||
LicensePaymentRecoveredEvent,
|
LicensePaymentRecoveredEvent,
|
||||||
|
PriceDuration,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
|
||||||
async function tierChanged(account: Account, from: number, to: number) {
|
async function planChanged(
|
||||||
const properties: LicenseTierChangedEvent = {
|
account: Account,
|
||||||
accountId: account.accountId,
|
opts: {
|
||||||
to,
|
from: PlanType
|
||||||
from,
|
to: PlanType
|
||||||
|
fromQuantity: number | undefined
|
||||||
|
toQuantity: number | undefined
|
||||||
|
fromDuration: PriceDuration | undefined
|
||||||
|
toDuration: PriceDuration | undefined
|
||||||
}
|
}
|
||||||
await publishEvent(Event.LICENSE_TIER_CHANGED, properties)
|
) {
|
||||||
}
|
|
||||||
|
|
||||||
async function planChanged(account: Account, from: PlanType, to: PlanType) {
|
|
||||||
const properties: LicensePlanChangedEvent = {
|
const properties: LicensePlanChangedEvent = {
|
||||||
accountId: account.accountId,
|
accountId: account.accountId,
|
||||||
to,
|
...opts,
|
||||||
from,
|
|
||||||
}
|
}
|
||||||
await publishEvent(Event.LICENSE_PLAN_CHANGED, properties)
|
await publishEvent(Event.LICENSE_PLAN_CHANGED, properties)
|
||||||
}
|
}
|
||||||
|
@ -74,7 +74,6 @@ async function paymentRecovered(account: Account) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
tierChanged,
|
|
||||||
planChanged,
|
planChanged,
|
||||||
activated,
|
activated,
|
||||||
checkoutOpened,
|
checkoutOpened,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export * as correlation from "./correlation/correlation"
|
export * as correlation from "./correlation/correlation"
|
||||||
export { logger, disableLogger } from "./pino/logger"
|
export { logger } from "./pino/logger"
|
||||||
export * from "./alerts"
|
export * from "./alerts"
|
||||||
|
|
||||||
// turn off or on context logging i.e. tenantId, appId etc
|
// turn off or on context logging i.e. tenantId, appId etc
|
||||||
|
|
|
@ -5,19 +5,10 @@ import * as correlation from "../correlation"
|
||||||
import { IdentityType } from "@budibase/types"
|
import { IdentityType } from "@budibase/types"
|
||||||
import { LOG_CONTEXT } from "../index"
|
import { LOG_CONTEXT } from "../index"
|
||||||
|
|
||||||
// CORE LOGGERS - for disabling
|
|
||||||
|
|
||||||
const BUILT_INS = {
|
|
||||||
log: console.log,
|
|
||||||
error: console.error,
|
|
||||||
info: console.info,
|
|
||||||
warn: console.warn,
|
|
||||||
trace: console.trace,
|
|
||||||
debug: console.debug,
|
|
||||||
}
|
|
||||||
|
|
||||||
// LOGGER
|
// LOGGER
|
||||||
|
|
||||||
|
let pinoInstance: pino.Logger | undefined
|
||||||
|
if (!env.DISABLE_PINO_LOGGER) {
|
||||||
const pinoOptions: LoggerOptions = {
|
const pinoOptions: LoggerOptions = {
|
||||||
level: env.LOG_LEVEL,
|
level: env.LOG_LEVEL,
|
||||||
formatters: {
|
formatters: {
|
||||||
|
@ -40,16 +31,7 @@ if (env.isDev()) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const logger = pino(pinoOptions)
|
pinoInstance = pino(pinoOptions)
|
||||||
|
|
||||||
export function disableLogger() {
|
|
||||||
console.log = BUILT_INS.log
|
|
||||||
console.error = BUILT_INS.error
|
|
||||||
console.info = BUILT_INS.info
|
|
||||||
console.warn = BUILT_INS.warn
|
|
||||||
console.trace = BUILT_INS.trace
|
|
||||||
console.debug = BUILT_INS.debug
|
|
||||||
}
|
|
||||||
|
|
||||||
// CONSOLE OVERRIDES
|
// CONSOLE OVERRIDES
|
||||||
|
|
||||||
|
@ -57,6 +39,7 @@ interface MergingObject {
|
||||||
objects?: any[]
|
objects?: any[]
|
||||||
tenantId?: string
|
tenantId?: string
|
||||||
appId?: string
|
appId?: string
|
||||||
|
automationId?: string
|
||||||
identityId?: string
|
identityId?: string
|
||||||
identityType?: IdentityType
|
identityType?: IdentityType
|
||||||
correlationId?: string
|
correlationId?: string
|
||||||
|
@ -104,36 +87,62 @@ function getLogParams(args: any[]): [MergingObject, string] {
|
||||||
contextObject = {
|
contextObject = {
|
||||||
tenantId: getTenantId(),
|
tenantId: getTenantId(),
|
||||||
appId: getAppId(),
|
appId: getAppId(),
|
||||||
|
automationId: getAutomationId(),
|
||||||
identityId: identity?._id,
|
identityId: identity?._id,
|
||||||
identityType: identity?.type,
|
identityType: identity?.type,
|
||||||
correlationId: correlation.getId(),
|
correlationId: correlation.getId(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mergingObject = {
|
const mergingObject: any = {
|
||||||
objects: objects.length ? objects : undefined,
|
|
||||||
err: error,
|
err: error,
|
||||||
...contextObject,
|
...contextObject,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (objects.length) {
|
||||||
|
// init generic data object for params supplied that don't have a
|
||||||
|
// '_logKey' field. This prints an object using argument index as the key
|
||||||
|
// e.g. { 0: {}, 1: {} }
|
||||||
|
const data: any = {}
|
||||||
|
let dataIndex = 0
|
||||||
|
|
||||||
|
for (let i = 0; i < objects.length; i++) {
|
||||||
|
const object = objects[i]
|
||||||
|
// the object has specified a log key
|
||||||
|
// use this instead of generic key
|
||||||
|
const logKey = object._logKey
|
||||||
|
if (logKey) {
|
||||||
|
delete object._logKey
|
||||||
|
mergingObject[logKey] = object
|
||||||
|
} else {
|
||||||
|
data[dataIndex] = object
|
||||||
|
dataIndex++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(data).length) {
|
||||||
|
mergingObject.data = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return [mergingObject, message]
|
return [mergingObject, message]
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log = (...arg: any[]) => {
|
console.log = (...arg: any[]) => {
|
||||||
const [obj, msg] = getLogParams(arg)
|
const [obj, msg] = getLogParams(arg)
|
||||||
logger.info(obj, msg)
|
pinoInstance?.info(obj, msg)
|
||||||
}
|
}
|
||||||
console.info = (...arg: any[]) => {
|
console.info = (...arg: any[]) => {
|
||||||
const [obj, msg] = getLogParams(arg)
|
const [obj, msg] = getLogParams(arg)
|
||||||
logger.info(obj, msg)
|
pinoInstance?.info(obj, msg)
|
||||||
}
|
}
|
||||||
console.warn = (...arg: any[]) => {
|
console.warn = (...arg: any[]) => {
|
||||||
const [obj, msg] = getLogParams(arg)
|
const [obj, msg] = getLogParams(arg)
|
||||||
logger.warn(obj, msg)
|
pinoInstance?.warn(obj, msg)
|
||||||
}
|
}
|
||||||
console.error = (...arg: any[]) => {
|
console.error = (...arg: any[]) => {
|
||||||
const [obj, msg] = getLogParams(arg)
|
const [obj, msg] = getLogParams(arg)
|
||||||
logger.error(obj, msg)
|
pinoInstance?.error(obj, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -147,12 +156,12 @@ console.trace = (...arg: any[]) => {
|
||||||
// to get stack trace
|
// to get stack trace
|
||||||
obj.err = new Error()
|
obj.err = new Error()
|
||||||
}
|
}
|
||||||
logger.trace(obj, msg)
|
pinoInstance?.trace(obj, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug = (...arg: any) => {
|
console.debug = (...arg: any) => {
|
||||||
const [obj, msg] = getLogParams(arg)
|
const [obj, msg] = getLogParams(arg)
|
||||||
logger.debug(obj, msg)
|
pinoInstance?.debug(obj, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CONTEXT
|
// CONTEXT
|
||||||
|
@ -177,6 +186,16 @@ const getAppId = () => {
|
||||||
return appId
|
return appId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getAutomationId = () => {
|
||||||
|
let appId
|
||||||
|
try {
|
||||||
|
appId = context.getAutomationId()
|
||||||
|
} catch (e) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
return appId
|
||||||
|
}
|
||||||
|
|
||||||
const getIdentity = () => {
|
const getIdentity = () => {
|
||||||
let identity
|
let identity
|
||||||
try {
|
try {
|
||||||
|
@ -186,3 +205,6 @@ const getIdentity = () => {
|
||||||
}
|
}
|
||||||
return identity
|
return identity
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const logger = pinoInstance
|
||||||
|
|
|
@ -128,6 +128,7 @@ class InMemoryQueue {
|
||||||
|
|
||||||
on() {
|
on() {
|
||||||
// do nothing
|
// do nothing
|
||||||
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitForCompletion() {
|
async waitForCompletion() {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Job, JobId, Queue } from "bull"
|
import { Job, JobId, Queue } from "bull"
|
||||||
import { JobQueue } from "./constants"
|
import { JobQueue } from "./constants"
|
||||||
|
import * as context from "../context"
|
||||||
|
|
||||||
export type StalledFn = (job: Job) => Promise<void>
|
export type StalledFn = (job: Job) => Promise<void>
|
||||||
|
|
||||||
|
@ -31,71 +32,164 @@ function handleStalled(queue: Queue, removeStalledCb?: StalledFn) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function logging(queue: Queue, jobQueue: JobQueue) {
|
function getLogParams(
|
||||||
let eventType: string
|
eventType: QueueEventType,
|
||||||
switch (jobQueue) {
|
event: BullEvent,
|
||||||
case JobQueue.AUTOMATION:
|
opts: {
|
||||||
eventType = "automation-event"
|
job?: Job
|
||||||
break
|
jobId?: JobId
|
||||||
case JobQueue.APP_BACKUP:
|
error?: Error
|
||||||
eventType = "app-backup-event"
|
} = {},
|
||||||
break
|
extra: any = {}
|
||||||
|
) {
|
||||||
|
const message = `[BULL] ${eventType}=${event}`
|
||||||
|
const err = opts.error
|
||||||
|
|
||||||
|
const bullLog = {
|
||||||
|
_logKey: "bull",
|
||||||
|
eventType,
|
||||||
|
event,
|
||||||
|
job: opts.job,
|
||||||
|
jobId: opts.jobId || opts.job?.id,
|
||||||
|
...extra,
|
||||||
}
|
}
|
||||||
if (process.env.NODE_DEBUG?.includes("bull")) {
|
|
||||||
|
let automationLog
|
||||||
|
if (opts.job?.data?.automation) {
|
||||||
|
automationLog = {
|
||||||
|
_logKey: "automation",
|
||||||
|
trigger: opts.job
|
||||||
|
? opts.job.data.automation.definition.trigger.event
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [message, err, bullLog, automationLog]
|
||||||
|
}
|
||||||
|
|
||||||
|
enum BullEvent {
|
||||||
|
ERROR = "error",
|
||||||
|
WAITING = "waiting",
|
||||||
|
ACTIVE = "active",
|
||||||
|
STALLED = "stalled",
|
||||||
|
PROGRESS = "progress",
|
||||||
|
COMPLETED = "completed",
|
||||||
|
FAILED = "failed",
|
||||||
|
PAUSED = "paused",
|
||||||
|
RESUMED = "resumed",
|
||||||
|
CLEANED = "cleaned",
|
||||||
|
DRAINED = "drained",
|
||||||
|
REMOVED = "removed",
|
||||||
|
}
|
||||||
|
|
||||||
|
enum QueueEventType {
|
||||||
|
AUTOMATION_EVENT = "automation-event",
|
||||||
|
APP_BACKUP_EVENT = "app-backup-event",
|
||||||
|
AUDIT_LOG_EVENT = "audit-log-event",
|
||||||
|
SYSTEM_EVENT = "system-event",
|
||||||
|
}
|
||||||
|
|
||||||
|
const EventTypeMap: { [key in JobQueue]: QueueEventType } = {
|
||||||
|
[JobQueue.AUTOMATION]: QueueEventType.AUTOMATION_EVENT,
|
||||||
|
[JobQueue.APP_BACKUP]: QueueEventType.APP_BACKUP_EVENT,
|
||||||
|
[JobQueue.AUDIT_LOG]: QueueEventType.AUDIT_LOG_EVENT,
|
||||||
|
[JobQueue.SYSTEM_EVENT_QUEUE]: QueueEventType.SYSTEM_EVENT,
|
||||||
|
}
|
||||||
|
|
||||||
|
function logging(queue: Queue, jobQueue: JobQueue) {
|
||||||
|
const eventType = EventTypeMap[jobQueue]
|
||||||
|
|
||||||
|
function doInJobContext(job: Job, task: any) {
|
||||||
|
// if this is an automation job try to get the app id
|
||||||
|
const appId = job.data.event?.appId
|
||||||
|
if (appId) {
|
||||||
|
return context.doInContext(appId, task)
|
||||||
|
} else {
|
||||||
|
task()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
queue
|
queue
|
||||||
.on("error", (error: any) => {
|
.on(BullEvent.STALLED, async (job: Job) => {
|
||||||
// An error occurred.
|
|
||||||
console.error(`${eventType}=error error=${JSON.stringify(error)}`)
|
|
||||||
})
|
|
||||||
.on("waiting", (jobId: JobId) => {
|
|
||||||
// A Job is waiting to be processed as soon as a worker is idling.
|
|
||||||
console.log(`${eventType}=waiting jobId=${jobId}`)
|
|
||||||
})
|
|
||||||
.on("active", (job: Job, jobPromise: any) => {
|
|
||||||
// A job has started. You can use `jobPromise.cancel()`` to abort it.
|
|
||||||
console.log(`${eventType}=active jobId=${job.id}`)
|
|
||||||
})
|
|
||||||
.on("stalled", (job: Job) => {
|
|
||||||
// A job has been marked as stalled. This is useful for debugging job
|
// A job has been marked as stalled. This is useful for debugging job
|
||||||
// workers that crash or pause the event loop.
|
// workers that crash or pause the event loop.
|
||||||
console.error(
|
await doInJobContext(job, () => {
|
||||||
`${eventType}=stalled jobId=${job.id} job=${JSON.stringify(job)}`
|
console.error(...getLogParams(eventType, BullEvent.STALLED, { job }))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.on(BullEvent.ERROR, (error: any) => {
|
||||||
|
// An error occurred.
|
||||||
|
console.error(...getLogParams(eventType, BullEvent.ERROR, { error }))
|
||||||
|
})
|
||||||
|
|
||||||
|
if (process.env.NODE_DEBUG?.includes("bull")) {
|
||||||
|
queue
|
||||||
|
.on(BullEvent.WAITING, (jobId: JobId) => {
|
||||||
|
// A Job is waiting to be processed as soon as a worker is idling.
|
||||||
|
console.info(...getLogParams(eventType, BullEvent.WAITING, { jobId }))
|
||||||
|
})
|
||||||
|
.on(BullEvent.ACTIVE, async (job: Job, jobPromise: any) => {
|
||||||
|
// A job has started. You can use `jobPromise.cancel()`` to abort it.
|
||||||
|
await doInJobContext(job, () => {
|
||||||
|
console.info(...getLogParams(eventType, BullEvent.ACTIVE, { job }))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.on(BullEvent.PROGRESS, async (job: Job, progress: any) => {
|
||||||
|
// A job's progress was updated
|
||||||
|
await doInJobContext(job, () => {
|
||||||
|
console.info(
|
||||||
|
...getLogParams(
|
||||||
|
eventType,
|
||||||
|
BullEvent.PROGRESS,
|
||||||
|
{ job },
|
||||||
|
{ progress }
|
||||||
|
)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.on("progress", (job: Job, progress: any) => {
|
|
||||||
// A job's progress was updated!
|
|
||||||
console.log(
|
|
||||||
`${eventType}=progress jobId=${job.id} progress=${progress}`
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.on("completed", (job: Job, result) => {
|
.on(BullEvent.COMPLETED, async (job: Job, result) => {
|
||||||
// A job successfully completed with a `result`.
|
// A job successfully completed with a `result`.
|
||||||
console.log(`${eventType}=completed jobId=${job.id} result=${result}`)
|
await doInJobContext(job, () => {
|
||||||
|
console.info(
|
||||||
|
...getLogParams(eventType, BullEvent.COMPLETED, { job }, { result })
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.on("failed", (job, err: any) => {
|
})
|
||||||
|
.on(BullEvent.FAILED, async (job: Job, error: any) => {
|
||||||
// A job failed with reason `err`!
|
// A job failed with reason `err`!
|
||||||
console.log(`${eventType}=failed jobId=${job.id} error=${err}`)
|
await doInJobContext(job, () => {
|
||||||
|
console.error(
|
||||||
|
...getLogParams(eventType, BullEvent.FAILED, { job, error })
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.on("paused", () => {
|
})
|
||||||
|
.on(BullEvent.PAUSED, () => {
|
||||||
// The queue has been paused.
|
// The queue has been paused.
|
||||||
console.log(`${eventType}=paused`)
|
console.info(...getLogParams(eventType, BullEvent.PAUSED))
|
||||||
})
|
})
|
||||||
.on("resumed", (job: Job) => {
|
.on(BullEvent.RESUMED, () => {
|
||||||
// The queue has been resumed.
|
// The queue has been resumed.
|
||||||
console.log(`${eventType}=paused jobId=${job.id}`)
|
console.info(...getLogParams(eventType, BullEvent.RESUMED))
|
||||||
})
|
})
|
||||||
.on("cleaned", (jobs: Job[], type: string) => {
|
.on(BullEvent.CLEANED, (jobs: Job[], type: string) => {
|
||||||
// Old jobs have been cleaned from the queue. `jobs` is an array of cleaned
|
// Old jobs have been cleaned from the queue. `jobs` is an array of cleaned
|
||||||
// jobs, and `type` is the type of jobs cleaned.
|
// jobs, and `type` is the type of jobs cleaned.
|
||||||
console.log(`${eventType}=cleaned length=${jobs.length} type=${type}`)
|
console.info(
|
||||||
|
...getLogParams(
|
||||||
|
eventType,
|
||||||
|
BullEvent.CLEANED,
|
||||||
|
{},
|
||||||
|
{ length: jobs.length, type }
|
||||||
|
)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.on("drained", () => {
|
.on(BullEvent.DRAINED, () => {
|
||||||
// Emitted every time the queue has processed all the waiting jobs (even if there can be some delayed jobs not yet processed)
|
// Emitted every time the queue has processed all the waiting jobs (even if there can be some delayed jobs not yet processed)
|
||||||
console.log(`${eventType}=drained`)
|
console.info(...getLogParams(eventType, BullEvent.DRAINED))
|
||||||
})
|
})
|
||||||
.on("removed", (job: Job) => {
|
.on(BullEvent.REMOVED, (job: Job) => {
|
||||||
// A job successfully removed.
|
// A job successfully removed.
|
||||||
console.log(`${eventType}=removed jobId=${job.id}`)
|
console.info(...getLogParams(eventType, BullEvent.REMOVED, { job }))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import * as timers from "../timers"
|
||||||
|
|
||||||
const RETRY_PERIOD_MS = 2000
|
const RETRY_PERIOD_MS = 2000
|
||||||
const STARTUP_TIMEOUT_MS = 5000
|
const STARTUP_TIMEOUT_MS = 5000
|
||||||
const CLUSTERED = false
|
const CLUSTERED = env.REDIS_CLUSTERED
|
||||||
const DEFAULT_SELECT_DB = SelectableDatabase.DEFAULT
|
const DEFAULT_SELECT_DB = SelectableDatabase.DEFAULT
|
||||||
|
|
||||||
// for testing just generate the client once
|
// for testing just generate the client once
|
||||||
|
@ -81,7 +81,7 @@ function init(selectDb = DEFAULT_SELECT_DB) {
|
||||||
if (client) {
|
if (client) {
|
||||||
client.disconnect()
|
client.disconnect()
|
||||||
}
|
}
|
||||||
const { redisProtocolUrl, opts, host, port } = getRedisOptions(CLUSTERED)
|
const { redisProtocolUrl, opts, host, port } = getRedisOptions()
|
||||||
|
|
||||||
if (CLUSTERED) {
|
if (CLUSTERED) {
|
||||||
client = new Redis.Cluster([{ host, port }], opts)
|
client = new Redis.Cluster([{ host, port }], opts)
|
||||||
|
|
|
@ -85,7 +85,7 @@ export const doWithLock = async <T>(
|
||||||
opts: LockOptions,
|
opts: LockOptions,
|
||||||
task: () => Promise<T>
|
task: () => Promise<T>
|
||||||
): Promise<RedlockExecution<T>> => {
|
): Promise<RedlockExecution<T>> => {
|
||||||
const redlock = await getClient(opts.type)
|
const redlock = await getClient(opts.type, opts.customOptions)
|
||||||
let lock
|
let lock
|
||||||
try {
|
try {
|
||||||
// determine lock name
|
// determine lock name
|
||||||
|
|
|
@ -57,7 +57,7 @@ export enum SelectableDatabase {
|
||||||
UNUSED_14 = 15,
|
UNUSED_14 = 15,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRedisOptions(clustered = false) {
|
export function getRedisOptions() {
|
||||||
let password = env.REDIS_PASSWORD
|
let password = env.REDIS_PASSWORD
|
||||||
let url: string[] | string = env.REDIS_URL.split("//")
|
let url: string[] | string = env.REDIS_URL.split("//")
|
||||||
// get rid of the protocol
|
// get rid of the protocol
|
||||||
|
@ -83,7 +83,7 @@ export function getRedisOptions(clustered = false) {
|
||||||
const opts: any = {
|
const opts: any = {
|
||||||
connectTimeout: CONNECT_TIMEOUT_MS,
|
connectTimeout: CONNECT_TIMEOUT_MS,
|
||||||
}
|
}
|
||||||
if (clustered) {
|
if (env.REDIS_CLUSTERED) {
|
||||||
opts.redisOptions = {}
|
opts.redisOptions = {}
|
||||||
opts.redisOptions.tls = {}
|
opts.redisOptions.tls = {}
|
||||||
opts.redisOptions.password = password
|
opts.redisOptions.password = password
|
||||||
|
|
|
@ -5,6 +5,7 @@ import * as db from "../../db"
|
||||||
import { Header } from "../../constants"
|
import { Header } from "../../constants"
|
||||||
import { newid } from "../../utils"
|
import { newid } from "../../utils"
|
||||||
import env from "../../environment"
|
import env from "../../environment"
|
||||||
|
import { BBContext } from "@budibase/types"
|
||||||
|
|
||||||
describe("utils", () => {
|
describe("utils", () => {
|
||||||
const config = new DBTestConfiguration()
|
const config = new DBTestConfiguration()
|
||||||
|
@ -106,4 +107,85 @@ describe("utils", () => {
|
||||||
expect(actual).toBe(undefined)
|
expect(actual).toBe(undefined)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("isServingBuilder", () => {
|
||||||
|
let ctx: BBContext
|
||||||
|
|
||||||
|
const expectResult = (result: boolean) =>
|
||||||
|
expect(utils.isServingBuilder(ctx)).toBe(result)
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
ctx = structures.koa.newContext()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns true if current path is in builder", async () => {
|
||||||
|
ctx.path = "/builder/app/app_"
|
||||||
|
expectResult(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns false if current path doesn't have '/' suffix", async () => {
|
||||||
|
ctx.path = "/builder/app"
|
||||||
|
expectResult(false)
|
||||||
|
|
||||||
|
ctx.path = "/xx"
|
||||||
|
expectResult(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("isServingBuilderPreview", () => {
|
||||||
|
let ctx: BBContext
|
||||||
|
|
||||||
|
const expectResult = (result: boolean) =>
|
||||||
|
expect(utils.isServingBuilderPreview(ctx)).toBe(result)
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
ctx = structures.koa.newContext()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns true if current path is in builder preview", async () => {
|
||||||
|
ctx.path = "/app/preview/xx"
|
||||||
|
expectResult(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns false if current path is not in builder preview", async () => {
|
||||||
|
ctx.path = "/builder"
|
||||||
|
expectResult(false)
|
||||||
|
|
||||||
|
ctx.path = "/xx"
|
||||||
|
expectResult(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("isPublicAPIRequest", () => {
|
||||||
|
let ctx: BBContext
|
||||||
|
|
||||||
|
const expectResult = (result: boolean) =>
|
||||||
|
expect(utils.isPublicApiRequest(ctx)).toBe(result)
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
ctx = structures.koa.newContext()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns true if current path remains to public API", async () => {
|
||||||
|
ctx.path = "/api/public/v1/invoices"
|
||||||
|
expectResult(true)
|
||||||
|
|
||||||
|
ctx.path = "/api/public/v1"
|
||||||
|
expectResult(true)
|
||||||
|
|
||||||
|
ctx.path = "/api/public/v2"
|
||||||
|
expectResult(true)
|
||||||
|
|
||||||
|
ctx.path = "/api/public/v21"
|
||||||
|
expectResult(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns false if current path doesn't remain to public API", async () => {
|
||||||
|
ctx.path = "/api/public"
|
||||||
|
expectResult(false)
|
||||||
|
|
||||||
|
ctx.path = "/xx"
|
||||||
|
expectResult(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
import { getAllApps, queryGlobalView } from "../db"
|
import { getAllApps } from "../db"
|
||||||
import {
|
import { Header, MAX_VALID_DATE, DocumentType, SEPARATOR } from "../constants"
|
||||||
Header,
|
|
||||||
MAX_VALID_DATE,
|
|
||||||
DocumentType,
|
|
||||||
SEPARATOR,
|
|
||||||
ViewName,
|
|
||||||
} from "../constants"
|
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
import * as tenancy from "../tenancy"
|
import * as tenancy from "../tenancy"
|
||||||
import * as context from "../context"
|
import * as context from "../context"
|
||||||
|
@ -23,7 +17,9 @@ const APP_PREFIX = DocumentType.APP + SEPARATOR
|
||||||
const PROD_APP_PREFIX = "/app/"
|
const PROD_APP_PREFIX = "/app/"
|
||||||
|
|
||||||
const BUILDER_PREVIEW_PATH = "/app/preview"
|
const BUILDER_PREVIEW_PATH = "/app/preview"
|
||||||
const BUILDER_REFERER_PREFIX = "/builder/app/"
|
const BUILDER_PREFIX = "/builder"
|
||||||
|
const BUILDER_APP_PREFIX = `${BUILDER_PREFIX}/app/`
|
||||||
|
const PUBLIC_API_PREFIX = "/api/public/v"
|
||||||
|
|
||||||
function confirmAppId(possibleAppId: string | undefined) {
|
function confirmAppId(possibleAppId: string | undefined) {
|
||||||
return possibleAppId && possibleAppId.startsWith(APP_PREFIX)
|
return possibleAppId && possibleAppId.startsWith(APP_PREFIX)
|
||||||
|
@ -69,6 +65,18 @@ export function isServingApp(ctx: Ctx) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isServingBuilder(ctx: Ctx): boolean {
|
||||||
|
return ctx.path.startsWith(BUILDER_APP_PREFIX)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isServingBuilderPreview(ctx: Ctx): boolean {
|
||||||
|
return ctx.path.startsWith(BUILDER_PREVIEW_PATH)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPublicApiRequest(ctx: Ctx): boolean {
|
||||||
|
return ctx.path.startsWith(PUBLIC_API_PREFIX)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a request tries to find the appId, which can be located in various places
|
* Given a request tries to find the appId, which can be located in various places
|
||||||
* @param {object} ctx The main request body to look through.
|
* @param {object} ctx The main request body to look through.
|
||||||
|
@ -110,7 +118,7 @@ export async function getAppIdFromCtx(ctx: Ctx) {
|
||||||
// make sure this is performed after prod app url resolution, in case the
|
// make sure this is performed after prod app url resolution, in case the
|
||||||
// referer header is present from a builder redirect
|
// referer header is present from a builder redirect
|
||||||
const referer = ctx.request.headers.referer
|
const referer = ctx.request.headers.referer
|
||||||
if (!appId && referer?.includes(BUILDER_REFERER_PREFIX)) {
|
if (!appId && referer?.includes(BUILDER_APP_PREFIX)) {
|
||||||
const refererId = parseAppIdFromUrl(ctx.request.headers.referer)
|
const refererId = parseAppIdFromUrl(ctx.request.headers.referer)
|
||||||
appId = confirmAppId(refererId)
|
appId = confirmAppId(refererId)
|
||||||
}
|
}
|
||||||
|
|
|
@ -123,7 +123,6 @@ beforeAll(async () => {
|
||||||
jest.spyOn(events.plugin, "imported")
|
jest.spyOn(events.plugin, "imported")
|
||||||
jest.spyOn(events.plugin, "deleted")
|
jest.spyOn(events.plugin, "deleted")
|
||||||
|
|
||||||
jest.spyOn(events.license, "tierChanged")
|
|
||||||
jest.spyOn(events.license, "planChanged")
|
jest.spyOn(events.license, "planChanged")
|
||||||
jest.spyOn(events.license, "activated")
|
jest.spyOn(events.license, "activated")
|
||||||
jest.spyOn(events.license, "checkoutOpened")
|
jest.spyOn(events.license, "checkoutOpened")
|
||||||
|
|
|
@ -7,16 +7,29 @@ import {
|
||||||
PlanType,
|
PlanType,
|
||||||
PriceDuration,
|
PriceDuration,
|
||||||
PurchasedPlan,
|
PurchasedPlan,
|
||||||
|
PurchasedPrice,
|
||||||
Quotas,
|
Quotas,
|
||||||
Subscription,
|
Subscription,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
|
||||||
|
export function price(): PurchasedPrice {
|
||||||
|
return {
|
||||||
|
amount: 10000,
|
||||||
|
amountMonthly: 10000,
|
||||||
|
currency: "usd",
|
||||||
|
duration: PriceDuration.MONTHLY,
|
||||||
|
priceId: "price_123",
|
||||||
|
dayPasses: undefined,
|
||||||
|
isPerUser: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const plan = (type: PlanType = PlanType.FREE): PurchasedPlan => {
|
export const plan = (type: PlanType = PlanType.FREE): PurchasedPlan => {
|
||||||
return {
|
return {
|
||||||
type,
|
type,
|
||||||
usesInvoicing: false,
|
usesInvoicing: false,
|
||||||
minUsers: 1,
|
|
||||||
model: PlanModel.PER_USER,
|
model: PlanModel.PER_USER,
|
||||||
|
price: type !== PlanType.FREE ? price() : undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,11 +7,6 @@
|
||||||
"@budibase/types": ["../types/src"]
|
"@budibase/types": ["../types/src"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"references": [
|
|
||||||
{ "path": "../types" }
|
"exclude": ["node_modules", "dist"]
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules",
|
|
||||||
"dist",
|
|
||||||
]
|
|
||||||
}
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/bbui",
|
"name": "@budibase/bbui",
|
||||||
"description": "A UI solution used in the different Budibase projects.",
|
"description": "A UI solution used in the different Budibase projects.",
|
||||||
"version": "2.5.6-alpha.30",
|
"version": "0.0.1",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"module": "dist/bbui.es.js",
|
"module": "dist/bbui.es.js",
|
||||||
|
@ -38,8 +38,8 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adobe/spectrum-css-workflow-icons": "1.2.1",
|
"@adobe/spectrum-css-workflow-icons": "1.2.1",
|
||||||
"@budibase/shared-core": "2.5.6-alpha.30",
|
"@budibase/shared-core": "0.0.1",
|
||||||
"@budibase/string-templates": "2.5.6-alpha.30",
|
"@budibase/string-templates": "0.0.1",
|
||||||
"@spectrum-css/accordion": "3.0.24",
|
"@spectrum-css/accordion": "3.0.24",
|
||||||
"@spectrum-css/actionbutton": "1.0.1",
|
"@spectrum-css/actionbutton": "1.0.1",
|
||||||
"@spectrum-css/actiongroup": "1.0.1",
|
"@spectrum-css/actiongroup": "1.0.1",
|
||||||
|
@ -84,7 +84,7 @@
|
||||||
"@spectrum-css/vars": "3.0.1",
|
"@spectrum-css/vars": "3.0.1",
|
||||||
"dayjs": "^1.10.4",
|
"dayjs": "^1.10.4",
|
||||||
"easymde": "^2.16.1",
|
"easymde": "^2.16.1",
|
||||||
"svelte-flatpickr": "^3.3.2",
|
"svelte-flatpickr": "3.2.3",
|
||||||
"svelte-portal": "^1.0.0"
|
"svelte-portal": "^1.0.0"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
const ignoredClasses = [".flatpickr-calendar", ".spectrum-Popover"]
|
const ignoredClasses = [
|
||||||
|
".flatpickr-calendar",
|
||||||
|
".spectrum-Popover",
|
||||||
|
".download-js-link",
|
||||||
|
]
|
||||||
let clickHandlers = []
|
let clickHandlers = []
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -22,8 +26,8 @@ const handleClick = event => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore clicks for modals, unless the handler is registered from a modal
|
// Ignore clicks for modals, unless the handler is registered from a modal
|
||||||
const sourceInModal = handler.anchor.closest(".spectrum-Modal") != null
|
const sourceInModal = handler.anchor.closest(".spectrum-Underlay") != null
|
||||||
const clickInModal = event.target.closest(".spectrum-Modal") != null
|
const clickInModal = event.target.closest(".spectrum-Underlay") != null
|
||||||
if (clickInModal && !sourceInModal) {
|
if (clickInModal && !sourceInModal) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import "@spectrum-css/button/dist/index-vars.css"
|
import "@spectrum-css/button/dist/index-vars.css"
|
||||||
import Tooltip from "../Tooltip/Tooltip.svelte"
|
import Tooltip from "../Tooltip/Tooltip.svelte"
|
||||||
|
|
||||||
|
export let type
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let size = "M"
|
export let size = "M"
|
||||||
export let cta = false
|
export let cta = false
|
||||||
|
@ -21,6 +22,7 @@
|
||||||
|
|
||||||
<button
|
<button
|
||||||
{id}
|
{id}
|
||||||
|
{type}
|
||||||
class:spectrum-Button--cta={cta}
|
class:spectrum-Button--cta={cta}
|
||||||
class:spectrum-Button--primary={primary}
|
class:spectrum-Button--primary={primary}
|
||||||
class:spectrum-Button--secondary={secondary}
|
class:spectrum-Button--secondary={secondary}
|
||||||
|
@ -73,6 +75,7 @@
|
||||||
button {
|
button {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spectrum-Button-label {
|
.spectrum-Button-label {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
|
@ -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>
|
<script>
|
||||||
import Icon from "../Icon/Icon.svelte"
|
import Icon from "../Icon/Icon.svelte"
|
||||||
import { getContext, onMount } from "svelte"
|
import { getContext, onMount } from "svelte"
|
||||||
import { slide } from "svelte/transition"
|
import ErrorMessage from "./ErrorMessage.svelte"
|
||||||
|
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let error = null
|
export let error = null
|
||||||
|
@ -55,9 +55,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if error}
|
{#if error}
|
||||||
<div transition:slide|local={{ duration: 130 }} class="error-message">
|
<ErrorMessage {error} />
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -110,13 +108,6 @@
|
||||||
.field {
|
.field {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
.error-message {
|
|
||||||
background: var(--spectrum-global-color-red-400);
|
|
||||||
color: white;
|
|
||||||
font-size: 14px;
|
|
||||||
padding: 6px 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
.error-icon {
|
.error-icon {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,3 +4,4 @@ export { default as FancySelect } from "./FancySelect.svelte"
|
||||||
export { default as FancyButton } from "./FancyButton.svelte"
|
export { default as FancyButton } from "./FancyButton.svelte"
|
||||||
export { default as FancyForm } from "./FancyForm.svelte"
|
export { default as FancyForm } from "./FancyForm.svelte"
|
||||||
export { default as FancyButtonRadio } from "./FancyButtonRadio.svelte"
|
export { default as FancyButtonRadio } from "./FancyButtonRadio.svelte"
|
||||||
|
export { default as ErrorMessage } from "./ErrorMessage.svelte"
|
||||||
|
|
|
@ -18,10 +18,14 @@
|
||||||
export let ignoreTimezones = false
|
export let ignoreTimezones = false
|
||||||
export let time24hr = false
|
export let time24hr = false
|
||||||
export let range = false
|
export let range = false
|
||||||
|
export let flatpickr
|
||||||
|
export let useKeyboardShortcuts = true
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const flatpickrId = `${uuid()}-wrapper`
|
const flatpickrId = `${uuid()}-wrapper`
|
||||||
|
|
||||||
let open = false
|
let open = false
|
||||||
let flatpickr, flatpickrOptions
|
let flatpickrOptions
|
||||||
|
|
||||||
// Another classic flatpickr issue. Errors were randomly being thrown due to
|
// Another classic flatpickr issue. Errors were randomly being thrown due to
|
||||||
// flatpickr internal code. Making sure that "destroy" is a valid function
|
// flatpickr internal code. Making sure that "destroy" is a valid function
|
||||||
|
@ -59,6 +63,8 @@
|
||||||
dispatch("change", timestamp.toISOString())
|
dispatch("change", timestamp.toISOString())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onOpen: () => dispatch("open"),
|
||||||
|
onClose: () => dispatch("close"),
|
||||||
}
|
}
|
||||||
|
|
||||||
$: redrawOptions = {
|
$: redrawOptions = {
|
||||||
|
@ -113,12 +119,16 @@
|
||||||
|
|
||||||
const onOpen = () => {
|
const onOpen = () => {
|
||||||
open = true
|
open = true
|
||||||
|
if (useKeyboardShortcuts) {
|
||||||
document.addEventListener("keyup", clearDateOnBackspace)
|
document.addEventListener("keyup", clearDateOnBackspace)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
open = false
|
open = false
|
||||||
|
if (useKeyboardShortcuts) {
|
||||||
document.removeEventListener("keyup", clearDateOnBackspace)
|
document.removeEventListener("keyup", clearDateOnBackspace)
|
||||||
|
}
|
||||||
|
|
||||||
// Manually blur all input fields since flatpickr creates a second
|
// Manually blur all input fields since flatpickr creates a second
|
||||||
// duplicate input field.
|
// duplicate input field.
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 26 KiB |
|
@ -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",
|
"name": "@budibase/builder",
|
||||||
"version": "2.5.6-alpha.30",
|
"version": "0.0.1",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -58,11 +58,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "2.5.6-alpha.30",
|
"@budibase/bbui": "0.0.1",
|
||||||
"@budibase/client": "2.5.6-alpha.30",
|
"@budibase/frontend-core": "0.0.1",
|
||||||
"@budibase/frontend-core": "2.5.6-alpha.30",
|
"@budibase/shared-core": "0.0.1",
|
||||||
"@budibase/shared-core": "2.5.6-alpha.30",
|
"@budibase/string-templates": "0.0.1",
|
||||||
"@budibase/string-templates": "2.5.6-alpha.30",
|
"@budibase/types": "0.0.1",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||||
|
|
|
@ -147,6 +147,9 @@ const automationActions = store => ({
|
||||||
testData,
|
testData,
|
||||||
})
|
})
|
||||||
if (!result?.trigger && !result?.steps?.length) {
|
if (!result?.trigger && !result?.steps?.length) {
|
||||||
|
if (result?.err?.code === "usage_limit_exceeded") {
|
||||||
|
throw "You have exceeded your automation quota"
|
||||||
|
}
|
||||||
throw "Something went wrong testing your automation"
|
throw "Something went wrong testing your automation"
|
||||||
}
|
}
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
|
|
|
@ -134,6 +134,7 @@ export const getFrontendStore = () => {
|
||||||
previousTopNavPath: {},
|
previousTopNavPath: {},
|
||||||
version: application.version,
|
version: application.version,
|
||||||
revertableVersion: application.revertableVersion,
|
revertableVersion: application.revertableVersion,
|
||||||
|
upgradableVersion: application.upgradableVersion,
|
||||||
navigation: application.navigation || {},
|
navigation: application.navigation || {},
|
||||||
usedPlugins: application.usedPlugins || [],
|
usedPlugins: application.usedPlugins || [],
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -94,7 +94,7 @@
|
||||||
/>
|
/>
|
||||||
<span class="icon-spacing">
|
<span class="icon-spacing">
|
||||||
<Body size="XS">
|
<Body size="XS">
|
||||||
{idx.charAt(0).toUpperCase() + idx.slice(1)}
|
{action.stepTitle || idx.charAt(0).toUpperCase() + idx.slice(1)}
|
||||||
</Body>
|
</Body>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import DiscordLogo from "assets/discord.svg"
|
import DiscordLogo from "assets/discord.svg"
|
||||||
import ZapierLogo from "assets/zapier.png"
|
import ZapierLogo from "assets/zapier.png"
|
||||||
import IntegromatLogo from "assets/integromat.png"
|
import MakeLogo from "assets/make.svg"
|
||||||
import SlackLogo from "assets/slack.svg"
|
import SlackLogo from "assets/slack.svg"
|
||||||
|
|
||||||
export const externalActions = {
|
export const externalActions = {
|
||||||
zapier: { name: "zapier", icon: ZapierLogo },
|
zapier: { name: "zapier", icon: ZapierLogo },
|
||||||
discord: { name: "discord", icon: DiscordLogo },
|
discord: { name: "discord", icon: DiscordLogo },
|
||||||
slack: { name: "slack", icon: SlackLogo },
|
slack: { name: "slack", icon: SlackLogo },
|
||||||
integromat: { name: "integromat", icon: IntegromatLogo },
|
integromat: { name: "integromat", icon: MakeLogo },
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,7 @@
|
||||||
await automationStore.actions.test($selectedAutomation, testData)
|
await automationStore.actions.test($selectedAutomation, testData)
|
||||||
$automationStore.showTestPanel = true
|
$automationStore.showTestPanel = true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error testing automation")
|
notifications.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -61,11 +61,63 @@
|
||||||
$: isTrigger = block?.type === "TRIGGER"
|
$: isTrigger = block?.type === "TRIGGER"
|
||||||
$: isUpdateRow = stepId === ActionStepID.UPDATE_ROW
|
$: isUpdateRow = stepId === ActionStepID.UPDATE_ROW
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO - Remove after November 2023
|
||||||
|
* *******************************
|
||||||
|
* Code added to provide backwards compatibility between Values 1,2,3,4,5
|
||||||
|
* and the new JSON body.
|
||||||
|
*/
|
||||||
|
let deprecatedSchemaProperties
|
||||||
|
$: {
|
||||||
|
if (block?.stepId === "integromat" || block?.stepId === "zapier") {
|
||||||
|
deprecatedSchemaProperties = schemaProperties.filter(
|
||||||
|
prop => !prop[0].startsWith("value")
|
||||||
|
)
|
||||||
|
if (!deprecatedSchemaProperties.map(entry => entry[0]).includes("body")) {
|
||||||
|
deprecatedSchemaProperties.push([
|
||||||
|
"body",
|
||||||
|
{
|
||||||
|
title: "Payload",
|
||||||
|
type: "json",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
deprecatedSchemaProperties = schemaProperties
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/****************************************************/
|
||||||
|
|
||||||
const getInputData = (testData, blockInputs) => {
|
const getInputData = (testData, blockInputs) => {
|
||||||
let newInputData = testData || blockInputs
|
let newInputData = testData || blockInputs
|
||||||
if (block.event === "app:trigger" && !newInputData?.fields) {
|
if (block.event === "app:trigger" && !newInputData?.fields) {
|
||||||
newInputData = cloneDeep(blockInputs)
|
newInputData = cloneDeep(blockInputs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO - Remove after November 2023
|
||||||
|
* *******************************
|
||||||
|
* Code added to provide backwards compatibility between Values 1,2,3,4,5
|
||||||
|
* and the new JSON body.
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
(block?.stepId === "integromat" || block?.stepId === "zapier") &&
|
||||||
|
!newInputData?.body?.value
|
||||||
|
) {
|
||||||
|
let deprecatedValues = {
|
||||||
|
...newInputData,
|
||||||
|
}
|
||||||
|
delete deprecatedValues.url
|
||||||
|
delete deprecatedValues.body
|
||||||
|
newInputData = {
|
||||||
|
url: newInputData.url,
|
||||||
|
body: {
|
||||||
|
value: JSON.stringify(deprecatedValues),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**********************************/
|
||||||
|
|
||||||
inputData = newInputData
|
inputData = newInputData
|
||||||
setDefaultEnumValues()
|
setDefaultEnumValues()
|
||||||
}
|
}
|
||||||
|
@ -239,7 +291,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
{#each schemaProperties as [key, value]}
|
{#each deprecatedSchemaProperties as [key, value]}
|
||||||
<div class="block-field">
|
<div class="block-field">
|
||||||
{#if key !== "fields"}
|
{#if key !== "fields"}
|
||||||
<Label
|
<Label
|
||||||
|
@ -256,6 +308,28 @@
|
||||||
options={value.enum}
|
options={value.enum}
|
||||||
getOptionLabel={(x, idx) => (value.pretty ? value.pretty[idx] : x)}
|
getOptionLabel={(x, idx) => (value.pretty ? value.pretty[idx] : x)}
|
||||||
/>
|
/>
|
||||||
|
{:else if value.type === "json"}
|
||||||
|
<Editor
|
||||||
|
editorHeight="250"
|
||||||
|
editorWidth="448"
|
||||||
|
mode="json"
|
||||||
|
value={inputData[key]?.value}
|
||||||
|
on:change={e => {
|
||||||
|
/**
|
||||||
|
* TODO - Remove after November 2023
|
||||||
|
* *******************************
|
||||||
|
* Code added to provide backwards compatibility between Values 1,2,3,4,5
|
||||||
|
* and the new JSON body.
|
||||||
|
*/
|
||||||
|
delete inputData.value1
|
||||||
|
delete inputData.value2
|
||||||
|
delete inputData.value3
|
||||||
|
delete inputData.value4
|
||||||
|
delete inputData.value5
|
||||||
|
/***********************/
|
||||||
|
onChange(e, key)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{:else if value.customType === "column"}
|
{:else if value.customType === "column"}
|
||||||
<Select
|
<Select
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange(e, key)}
|
||||||
|
|
|
@ -22,6 +22,8 @@
|
||||||
export let rowCount
|
export let rowCount
|
||||||
export let disableSorting = false
|
export let disableSorting = false
|
||||||
export let customPlaceholder = false
|
export let customPlaceholder = false
|
||||||
|
export let allowClickRows
|
||||||
|
export let allowEditing = true
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -109,7 +111,9 @@
|
||||||
{rowCount}
|
{rowCount}
|
||||||
{disableSorting}
|
{disableSorting}
|
||||||
{customPlaceholder}
|
{customPlaceholder}
|
||||||
|
allowEditRows={allowEditing}
|
||||||
showAutoColumns={!hideAutocolumns}
|
showAutoColumns={!hideAutocolumns}
|
||||||
|
{allowClickRows}
|
||||||
on:clickrelationship={e => selectRelationship(e.detail)}
|
on:clickrelationship={e => selectRelationship(e.detail)}
|
||||||
on:sort
|
on:sort
|
||||||
>
|
>
|
||||||
|
|
|
@ -58,6 +58,7 @@
|
||||||
{loading}
|
{loading}
|
||||||
{type}
|
{type}
|
||||||
rowCount={10}
|
rowCount={10}
|
||||||
|
allowEditing={false}
|
||||||
bind:hideAutocolumns
|
bind:hideAutocolumns
|
||||||
>
|
>
|
||||||
<ViewFilterButton {view} />
|
<ViewFilterButton {view} />
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
{#if datasource}
|
{#if datasource}
|
||||||
<div>
|
<div>
|
||||||
<ActionButton icon="DataCorrelated" primary quiet on:click={modal.show}>
|
<ActionButton icon="DataCorrelated" primary quiet on:click={modal.show}>
|
||||||
Define existing relationship
|
Define relationship
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
$: selectedRowArray = Object.keys($selectedRows).map(id => ({ _id: id }))
|
$: selectedRowArray = Object.keys($selectedRows).map(id => ({ _id: id }))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<span data-ignore-click-outside="true">
|
||||||
<ExportButton
|
<ExportButton
|
||||||
{disabled}
|
{disabled}
|
||||||
view={$tableId}
|
view={$tableId}
|
||||||
|
@ -19,3 +20,10 @@
|
||||||
}}
|
}}
|
||||||
selectedRows={selectedRowArray}
|
selectedRows={selectedRowArray}
|
||||||
/>
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
span {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -2,19 +2,19 @@
|
||||||
import TableFilterButton from "../TableFilterButton.svelte"
|
import TableFilterButton from "../TableFilterButton.svelte"
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
const { columns, config, filter, table } = getContext("grid")
|
const { columns, tableId, filter, table } = getContext("grid")
|
||||||
|
|
||||||
const onFilter = e => {
|
const onFilter = e => {
|
||||||
filter.set(e.detail || [])
|
filter.set(e.detail || [])
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#key $config.tableId}
|
{#key $tableId}
|
||||||
<TableFilterButton
|
<TableFilterButton
|
||||||
schema={$table?.schema}
|
schema={$table?.schema}
|
||||||
filters={$filter}
|
filters={$filter}
|
||||||
on:change={onFilter}
|
on:change={onFilter}
|
||||||
disabled={!$columns.length}
|
disabled={!$columns.length}
|
||||||
tableId={$config.tableId}
|
tableId={$tableId}
|
||||||
/>
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import ManageAccessButton from "../ManageAccessButton.svelte"
|
import ManageAccessButton from "../ManageAccessButton.svelte"
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
const { config } = getContext("grid")
|
const { tableId } = getContext("grid")
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ManageAccessButton resourceId={$config.tableId} />
|
<ManageAccessButton resourceId={$tableId} />
|
||||||
|
|
|
@ -6,7 +6,8 @@
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
import { goto, isActive } from "@roxi/routify"
|
import { goto, isActive } from "@roxi/routify"
|
||||||
|
|
||||||
const alphabetical = (a, b) => a.name?.toLowerCase() > b.name?.toLowerCase()
|
const alphabetical = (a, b) =>
|
||||||
|
a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1
|
||||||
|
|
||||||
export let sourceId
|
export let sourceId
|
||||||
|
|
||||||
|
|
|
@ -42,16 +42,7 @@ export const parseFile = e => {
|
||||||
|
|
||||||
reader.addEventListener("load", function (e) {
|
reader.addEventListener("load", function (e) {
|
||||||
const fileData = e.target.result
|
const fileData = e.target.result
|
||||||
|
if (file.type?.includes("json")) {
|
||||||
if (file.type === "text/csv") {
|
|
||||||
API.csvToJson(fileData)
|
|
||||||
.then(rows => {
|
|
||||||
resolveRows(rows)
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
reject("can't convert csv to json")
|
|
||||||
})
|
|
||||||
} else if (file.type === "application/json") {
|
|
||||||
const parsedFileData = JSON.parse(fileData)
|
const parsedFileData = JSON.parse(fileData)
|
||||||
|
|
||||||
if (Array.isArray(parsedFileData)) {
|
if (Array.isArray(parsedFileData)) {
|
||||||
|
@ -62,7 +53,13 @@ export const parseFile = e => {
|
||||||
reject("invalid json format")
|
reject("invalid json format")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
reject("invalid file type")
|
API.csvToJson(fileData)
|
||||||
|
.then(rows => {
|
||||||
|
resolveRows(rows)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
reject("cannot parse csv")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
export let highlighted = false
|
export let highlighted = false
|
||||||
export let rightAlignIcon = false
|
export let rightAlignIcon = false
|
||||||
export let id
|
export let id
|
||||||
|
export let showTooltip = false
|
||||||
|
|
||||||
const scrollApi = getContext("scroll")
|
const scrollApi = getContext("scroll")
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
@ -84,7 +85,7 @@
|
||||||
<Icon color={iconColor} size="S" name={icon} />
|
<Icon color={iconColor} size="S" name={icon} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="text">{text}</div>
|
<div class="text" title={showTooltip ? text : null}>{text}</div>
|
||||||
{#if withActions}
|
{#if withActions}
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import clientPackage from "@budibase/client/package.json"
|
|
||||||
|
|
||||||
export function show() {
|
export function show() {
|
||||||
updateModal.show()
|
updateModal.show()
|
||||||
|
@ -25,9 +24,9 @@
|
||||||
|
|
||||||
$: appId = $store.appId
|
$: appId = $store.appId
|
||||||
$: updateAvailable =
|
$: updateAvailable =
|
||||||
clientPackage.version &&
|
$store.upgradableVersion &&
|
||||||
$store.version &&
|
$store.version &&
|
||||||
clientPackage.version !== $store.version
|
$store.upgradableVersion !== $store.version
|
||||||
$: revertAvailable = $store.revertableVersion != null
|
$: revertAvailable = $store.revertableVersion != null
|
||||||
|
|
||||||
const refreshAppPackage = async () => {
|
const refreshAppPackage = async () => {
|
||||||
|
@ -46,7 +45,7 @@
|
||||||
// Don't wait for the async refresh, since this causes modal flashing
|
// Don't wait for the async refresh, since this causes modal flashing
|
||||||
refreshAppPackage()
|
refreshAppPackage()
|
||||||
notifications.success(
|
notifications.success(
|
||||||
`App updated successfully to version ${clientPackage.version}`
|
`App updated successfully to version ${$store.upgradableVersion}`
|
||||||
)
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifications.error(`Error updating app: ${err}`)
|
notifications.error(`Error updating app: ${err}`)
|
||||||
|
@ -91,7 +90,7 @@
|
||||||
{#if updateAvailable}
|
{#if updateAvailable}
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
This app is currently using version <b>{$store.version}</b>, but version
|
This app is currently using version <b>{$store.version}</b>, but version
|
||||||
<b>{clientPackage.version}</b> is available. Updates can contain new features,
|
<b>{$store.upgradableVersion}</b> is available. Updates can contain new features,
|
||||||
performance improvements and bug fixes.
|
performance improvements and bug fixes.
|
||||||
</Body>
|
</Body>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
export let tab = true
|
export let tab = true
|
||||||
export let mode
|
export let mode
|
||||||
export let editorHeight = 500
|
export let editorHeight = 500
|
||||||
|
export let editorWidth = 640
|
||||||
// export let parameters = []
|
// export let parameters = []
|
||||||
|
|
||||||
let width
|
let width
|
||||||
|
@ -169,7 +170,9 @@
|
||||||
{#if label}
|
{#if label}
|
||||||
<Label small>{label}</Label>
|
<Label small>{label}</Label>
|
||||||
{/if}
|
{/if}
|
||||||
<div style={`--code-mirror-height: ${editorHeight}px`}>
|
<div
|
||||||
|
style={`--code-mirror-height: ${editorHeight}px; --code-mirror-width: ${editorWidth}px;`}
|
||||||
|
>
|
||||||
<textarea tabindex="0" bind:this={refs.editor} readonly {value} />
|
<textarea tabindex="0" bind:this={refs.editor} readonly {value} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -183,6 +186,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.CodeMirror) {
|
div :global(.CodeMirror) {
|
||||||
|
width: var(--code-mirror-width) !important;
|
||||||
height: var(--code-mirror-height) !important;
|
height: var(--code-mirror-height) !important;
|
||||||
border-radius: var(--border-radius-s);
|
border-radius: var(--border-radius-s);
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
|
|
|
@ -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 { admin, auth, licensing } from "stores/portal"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { BANNER_TYPES } from "@budibase/bbui"
|
import { BANNER_TYPES } from "@budibase/bbui"
|
||||||
import { capitalise } from "helpers"
|
|
||||||
|
|
||||||
const oneDayInSeconds = 86400
|
const oneDayInSeconds = 86400
|
||||||
|
|
||||||
|
@ -146,20 +145,19 @@ const buildUsersAboveLimitBanner = EXPIRY_KEY => {
|
||||||
const userLicensing = get(licensing)
|
const userLicensing = get(licensing)
|
||||||
return {
|
return {
|
||||||
key: EXPIRY_KEY,
|
key: EXPIRY_KEY,
|
||||||
type: BANNER_TYPES.WARNING,
|
type: BANNER_TYPES.NEGATIVE,
|
||||||
criteria: () => {
|
onChange: () => {
|
||||||
return userLicensing.warnUserLimit
|
defaultCacheFn(EXPIRY_KEY)
|
||||||
},
|
},
|
||||||
message: `${capitalise(
|
criteria: () => {
|
||||||
userLicensing.license.plan.type
|
return userLicensing.errUserLimit
|
||||||
)} plan changes - Users will be limited to ${
|
},
|
||||||
userLicensing.userLimit
|
message: "Your Budibase account is de-activated. Upgrade your plan",
|
||||||
} users in ${userLicensing.userLimitDays}`,
|
|
||||||
...{
|
...{
|
||||||
extraButtonText: "Find out more",
|
extraButtonText: "View plans",
|
||||||
extraButtonAction: () => {
|
extraButtonAction: () => {
|
||||||
defaultCacheFn(ExpiringKeys.LICENSING_USERS_ABOVE_LIMIT_BANNER)
|
defaultCacheFn(ExpiringKeys.LICENSING_USERS_ABOVE_LIMIT_BANNER)
|
||||||
window.location.href = "/builder/portal/users/users"
|
window.location.href = "https://budibase.com/pricing/"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
showCloseButton: true,
|
showCloseButton: true,
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
|
|
||||||
export let app
|
export let app
|
||||||
|
|
||||||
|
export let lockedAction
|
||||||
|
|
||||||
const handleDefaultClick = () => {
|
const handleDefaultClick = () => {
|
||||||
if (window.innerWidth < 640) {
|
if (window.innerWidth < 640) {
|
||||||
goToOverview()
|
goToOverview()
|
||||||
|
@ -29,7 +31,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="app-row" on:click={handleDefaultClick}>
|
<div class="app-row" on:click={lockedAction || handleDefaultClick}>
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<div class="app-icon">
|
<div class="app-icon">
|
||||||
<Icon size="L" name={app.icon?.name || "Apps"} color={app.icon?.color} />
|
<Icon size="L" name={app.icon?.name || "Apps"} color={app.icon?.color} />
|
||||||
|
@ -58,8 +60,11 @@
|
||||||
|
|
||||||
<div class="app-row-actions">
|
<div class="app-row-actions">
|
||||||
<AppLockModal {app} buttonSize="M" />
|
<AppLockModal {app} buttonSize="M" />
|
||||||
<Button size="S" secondary on:click={goToOverview}>Manage</Button>
|
<Button size="S" secondary on:click={lockedAction || goToOverview}
|
||||||
<Button size="S" primary on:click={goToBuilder}>Edit</Button>
|
>Manage</Button
|
||||||
|
>
|
||||||
|
<Button size="S" primary on:click={lockedAction || goToBuilder}>Edit</Button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
unlimited = isUnlimited()
|
unlimited = isUnlimited()
|
||||||
percentage = getPercentage()
|
percentage = getPercentage()
|
||||||
if (warnWhenFull && percentage === 100) {
|
if (warnWhenFull && percentage >= 100) {
|
||||||
showWarning = true
|
showWarning = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -28,13 +28,16 @@
|
||||||
let inviting = false
|
let inviting = false
|
||||||
let searchFocus = false
|
let searchFocus = false
|
||||||
|
|
||||||
|
// Initially filter entities without app access
|
||||||
|
// Show all when false
|
||||||
|
let filterByAppAccess = true
|
||||||
|
|
||||||
let appInvites = []
|
let appInvites = []
|
||||||
let filteredInvites = []
|
let filteredInvites = []
|
||||||
let filteredUsers = []
|
let filteredUsers = []
|
||||||
let filteredGroups = []
|
let filteredGroups = []
|
||||||
let selectedGroup
|
let selectedGroup
|
||||||
let userOnboardResponse = null
|
let userOnboardResponse = null
|
||||||
|
|
||||||
let userLimitReachedModal
|
let userLimitReachedModal
|
||||||
|
|
||||||
$: queryIsEmail = emailValidator(query) === true
|
$: queryIsEmail = emailValidator(query) === true
|
||||||
|
@ -52,15 +55,32 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterInvites = async query => {
|
const filterInvites = async query => {
|
||||||
appInvites = await getInvites()
|
if (!prodAppId) {
|
||||||
if (!query || query == "") {
|
|
||||||
filteredInvites = appInvites
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
filteredInvites = appInvites.filter(invite => invite.email.includes(query))
|
|
||||||
|
appInvites = await getInvites()
|
||||||
|
|
||||||
|
//On Focus behaviour
|
||||||
|
if (!filterByAppAccess && !query) {
|
||||||
|
filteredInvites =
|
||||||
|
appInvites.length > 100 ? appInvites.slice(0, 100) : [...appInvites]
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
$: filterInvites(query)
|
filteredInvites = appInvites.filter(invite => {
|
||||||
|
const inviteInfo = invite.info?.apps
|
||||||
|
if (!query && inviteInfo && prodAppId) {
|
||||||
|
return Object.keys(inviteInfo).includes(prodAppId)
|
||||||
|
}
|
||||||
|
return invite.email.includes(query)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$: filterByAppAccess, prodAppId, filterInvites(query)
|
||||||
|
$: if (searchFocus === true) {
|
||||||
|
filterByAppAccess = false
|
||||||
|
}
|
||||||
|
|
||||||
const usersFetch = fetchData({
|
const usersFetch = fetchData({
|
||||||
API,
|
API,
|
||||||
|
@ -79,9 +99,9 @@
|
||||||
}
|
}
|
||||||
await usersFetch.update({
|
await usersFetch.update({
|
||||||
query: {
|
query: {
|
||||||
appId: query ? null : prodAppId,
|
appId: query || !filterByAppAccess ? null : prodAppId,
|
||||||
email: query,
|
email: query,
|
||||||
paginated: query ? null : false,
|
paginated: query || !filterByAppAccess ? null : false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
await usersFetch.refresh()
|
await usersFetch.refresh()
|
||||||
|
@ -107,7 +127,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const debouncedUpdateFetch = Utils.debounce(searchUsers, 250)
|
const debouncedUpdateFetch = Utils.debounce(searchUsers, 250)
|
||||||
$: debouncedUpdateFetch(query, $store.builderSidePanel, loaded)
|
$: debouncedUpdateFetch(
|
||||||
|
query,
|
||||||
|
$store.builderSidePanel,
|
||||||
|
loaded,
|
||||||
|
filterByAppAccess
|
||||||
|
)
|
||||||
|
|
||||||
const updateAppUser = async (user, role) => {
|
const updateAppUser = async (user, role) => {
|
||||||
if (!prodAppId) {
|
if (!prodAppId) {
|
||||||
|
@ -182,7 +207,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchGroups = (userGroups, query) => {
|
const searchGroups = (userGroups, query) => {
|
||||||
let filterGroups = query?.length
|
let filterGroups =
|
||||||
|
query?.length || !filterByAppAccess
|
||||||
? userGroups
|
? userGroups
|
||||||
: getAppGroups(userGroups, prodAppId)
|
: getAppGroups(userGroups, prodAppId)
|
||||||
return filterGroups
|
return filterGroups
|
||||||
|
@ -214,7 +240,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds the 'role' attribute and sets it to the current app.
|
// Adds the 'role' attribute and sets it to the current app.
|
||||||
$: enrichedGroups = getEnrichedGroups($groups)
|
$: enrichedGroups = getEnrichedGroups($groups, filterByAppAccess)
|
||||||
$: filteredGroups = searchGroups(enrichedGroups, query)
|
$: filteredGroups = searchGroups(enrichedGroups, query)
|
||||||
$: groupUsers = buildGroupUsers(filteredGroups, filteredUsers)
|
$: groupUsers = buildGroupUsers(filteredGroups, filteredUsers)
|
||||||
$: allUsers = [...filteredUsers, ...groupUsers]
|
$: allUsers = [...filteredUsers, ...groupUsers]
|
||||||
|
@ -226,7 +252,7 @@
|
||||||
specific roles for the app.
|
specific roles for the app.
|
||||||
*/
|
*/
|
||||||
const buildGroupUsers = (userGroups, filteredUsers) => {
|
const buildGroupUsers = (userGroups, filteredUsers) => {
|
||||||
if (query) {
|
if (query || !filterByAppAccess) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
// Must exclude users who have explicit privileges
|
// Must exclude users who have explicit privileges
|
||||||
|
@ -321,12 +347,12 @@
|
||||||
[prodAppId]: role,
|
[prodAppId]: role,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
await filterInvites()
|
await filterInvites(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onUninviteAppUser = async invite => {
|
const onUninviteAppUser = async invite => {
|
||||||
await uninviteAppUser(invite)
|
await uninviteAppUser(invite)
|
||||||
await filterInvites()
|
await filterInvites(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Purge only the app from the invite or recind the invite if only 1 app remains?
|
// Purge only the app from the invite or recind the invite if only 1 app remains?
|
||||||
|
@ -351,7 +377,6 @@
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
rendered = true
|
rendered = true
|
||||||
searchFocus = true
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function handleKeyDown(evt) {
|
function handleKeyDown(evt) {
|
||||||
|
@ -417,7 +442,6 @@
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
disabled={inviting}
|
disabled={inviting}
|
||||||
value={query}
|
value={query}
|
||||||
autofocus
|
|
||||||
on:input={e => {
|
on:input={e => {
|
||||||
query = e.target.value.trim()
|
query = e.target.value.trim()
|
||||||
}}
|
}}
|
||||||
|
@ -428,16 +452,20 @@
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class="search-input-icon"
|
class="search-input-icon"
|
||||||
class:searching={query}
|
class:searching={query || !filterByAppAccess}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
if (!filterByAppAccess) {
|
||||||
|
filterByAppAccess = true
|
||||||
|
}
|
||||||
if (!query) {
|
if (!query) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
query = null
|
query = null
|
||||||
userOnboardResponse = null
|
userOnboardResponse = null
|
||||||
|
filterByAppAccess = true
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon name={query ? "Close" : "Search"} />
|
<Icon name={!filterByAppAccess || query ? "Close" : "Search"} />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -696,7 +724,7 @@
|
||||||
max-width: calc(100vw - 40px);
|
max-width: calc(100vw - 40px);
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
border-left: var(--border-light);
|
border-left: var(--border-light);
|
||||||
z-index: 3;
|
z-index: 999;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Panel title={$selectedLayout?.name} icon="Experience" borderLeft>
|
<Panel title={$selectedLayout?.name} icon="Experience" borderLeft wide>
|
||||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
<Layout paddingX="L" paddingY="XL" gap="S">
|
||||||
<Banner type="warning" showCloseButton={false}>
|
<Banner type="warning" showCloseButton={false}>
|
||||||
Custom layouts are being deprecated. They will be removed in a future
|
Custom layouts are being deprecated. They will be removed in a future
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Panel borderLeft title="Navigation" icon="InfoOutline">
|
<Panel borderLeft title="Navigation" icon="InfoOutline" wide>
|
||||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
<Layout paddingX="L" paddingY="XL" gap="S">
|
||||||
{#if $selectedScreen.layoutId}
|
{#if $selectedScreen.layoutId}
|
||||||
<Banner
|
<Banner
|
||||||
|
|
|
@ -59,6 +59,7 @@
|
||||||
text={screen.routing.route}
|
text={screen.routing.route}
|
||||||
on:click={() => store.actions.screens.select(screen._id)}
|
on:click={() => store.actions.screens.select(screen._id)}
|
||||||
rightAlignIcon
|
rightAlignIcon
|
||||||
|
showTooltip
|
||||||
>
|
>
|
||||||
<ScreenDropdownMenu screenId={screen._id} />
|
<ScreenDropdownMenu screenId={screen._id} />
|
||||||
<RoleIndicator slot="right" roleId={screen.routing.roleId} />
|
<RoleIndicator slot="right" roleId={screen.routing.roleId} />
|
||||||
|
|
|
@ -149,6 +149,7 @@
|
||||||
title={$selectedScreen.routing.route}
|
title={$selectedScreen.routing.route}
|
||||||
icon={$selectedScreen.routing.route === "/" ? "Home" : "WebPage"}
|
icon={$selectedScreen.routing.route === "/" ? "Home" : "WebPage"}
|
||||||
borderLeft
|
borderLeft
|
||||||
|
wide
|
||||||
>
|
>
|
||||||
<Layout gap="S" paddingX="L" paddingY="XL">
|
<Layout gap="S" paddingX="L" paddingY="XL">
|
||||||
{#if $selectedScreen.layoutId}
|
{#if $selectedScreen.layoutId}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import { Body, Layout } from "@budibase/bbui"
|
import { Body, Layout } from "@budibase/bbui"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Panel borderLeft title="Theme" icon="InfoOutline">
|
<Panel borderLeft title="Theme" icon="InfoOutline" wide>
|
||||||
<Layout paddingX="L" paddingY="XL">
|
<Layout paddingX="L" paddingY="XL">
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
Your theme is set across all the screens within your app.
|
Your theme is set across all the screens within your app.
|
||||||
|
|
|
@ -133,7 +133,7 @@
|
||||||
</Body>
|
</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
<Divider />
|
<Divider />
|
||||||
{#if $licensing.usageMetrics?.dayPasses >= 100}
|
{#if $licensing.usageMetrics?.dayPasses >= 100 || $licensing.errUserLimit}
|
||||||
<div>
|
<div>
|
||||||
<Layout gap="S" justifyItems="center">
|
<Layout gap="S" justifyItems="center">
|
||||||
<img class="spaceman" alt="spaceman" src={Spaceman} />
|
<img class="spaceman" alt="spaceman" src={Spaceman} />
|
||||||
|
|
|
@ -43,12 +43,18 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
$: quotaUsage = $licensing.quotaUsage
|
$: quotaUsage = $licensing.quotaUsage
|
||||||
|
|
||||||
$: license = $auth.user?.license
|
$: license = $auth.user?.license
|
||||||
|
$: plan = license?.plan
|
||||||
|
$: usesInvoicing = plan?.usesInvoicing
|
||||||
|
|
||||||
$: accountPortalAccess = $auth?.user?.accountPortalAccess
|
$: accountPortalAccess = $auth?.user?.accountPortalAccess
|
||||||
$: quotaReset = quotaUsage?.quotaReset
|
$: quotaReset = quotaUsage?.quotaReset
|
||||||
$: canManagePlan =
|
$: canManagePlan =
|
||||||
($admin.cloud && accountPortalAccess) || (!$admin.cloud && $auth.isAdmin)
|
($admin.cloud && accountPortalAccess) || (!$admin.cloud && $auth.isAdmin)
|
||||||
|
|
||||||
|
$: showButton = !usesInvoicing && accountPortalAccess
|
||||||
|
|
||||||
const setMonthlyUsage = () => {
|
const setMonthlyUsage = () => {
|
||||||
monthlyUsage = []
|
monthlyUsage = []
|
||||||
if (quotaUsage.monthly) {
|
if (quotaUsage.monthly) {
|
||||||
|
@ -121,7 +127,7 @@
|
||||||
const setTextRows = () => {
|
const setTextRows = () => {
|
||||||
textRows = []
|
textRows = []
|
||||||
|
|
||||||
if (cancelAt) {
|
if (cancelAt && !usesInvoicing) {
|
||||||
textRows.push({ message: "Subscription has been cancelled" })
|
textRows.push({ message: "Subscription has been cancelled" })
|
||||||
textRows.push({
|
textRows.push({
|
||||||
message: `${getDaysRemaining(cancelAt)} days remaining`,
|
message: `${getDaysRemaining(cancelAt)} days remaining`,
|
||||||
|
@ -213,7 +219,7 @@
|
||||||
description="YOUR CURRENT PLAN"
|
description="YOUR CURRENT PLAN"
|
||||||
title={planTitle()}
|
title={planTitle()}
|
||||||
{primaryActionText}
|
{primaryActionText}
|
||||||
primaryAction={accountPortalAccess ? goToAccountPortal : undefined}
|
primaryAction={showButton ? goToAccountPortal : undefined}
|
||||||
{textRows}
|
{textRows}
|
||||||
>
|
>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
@ -224,12 +230,6 @@
|
||||||
<Usage {usage} warnWhenFull={WARN_USAGE.includes(usage.name)} />
|
<Usage {usage} warnWhenFull={WARN_USAGE.includes(usage.name)} />
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</Layout>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if monthlyUsage.length}
|
|
||||||
<div class="column">
|
|
||||||
<Layout noPadding gap="M">
|
|
||||||
<Layout gap="XS" noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
<Heading size="S">Monthly limits</Heading>
|
<Heading size="S">Monthly limits</Heading>
|
||||||
<div class="detail">
|
<div class="detail">
|
||||||
|
@ -242,15 +242,11 @@
|
||||||
</Layout>
|
</Layout>
|
||||||
<Layout noPadding gap="M">
|
<Layout noPadding gap="M">
|
||||||
{#each monthlyUsage as usage}
|
{#each monthlyUsage as usage}
|
||||||
<Usage
|
<Usage {usage} warnWhenFull={WARN_USAGE.includes(usage.name)} />
|
||||||
{usage}
|
|
||||||
warnWhenFull={WARN_USAGE.includes(usage.name)}
|
|
||||||
/>
|
|
||||||
{/each}
|
{/each}
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</DashCard>
|
</DashCard>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
import Spinner from "components/common/Spinner.svelte"
|
import Spinner from "components/common/Spinner.svelte"
|
||||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||||
import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte"
|
import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte"
|
||||||
|
import AccountLockedModal from "components/portal/licensing/AccountLockedModal.svelte"
|
||||||
|
|
||||||
import { store, automationStore } from "builderStore"
|
import { store, automationStore } from "builderStore"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
@ -28,6 +29,7 @@
|
||||||
let template
|
let template
|
||||||
let creationModal
|
let creationModal
|
||||||
let appLimitModal
|
let appLimitModal
|
||||||
|
let accountLockedModal
|
||||||
let creatingApp = false
|
let creatingApp = false
|
||||||
let searchTerm = ""
|
let searchTerm = ""
|
||||||
let creatingFromTemplate = false
|
let creatingFromTemplate = false
|
||||||
|
@ -48,6 +50,11 @@
|
||||||
: true)
|
: true)
|
||||||
)
|
)
|
||||||
$: automationErrors = getAutomationErrors(enrichedApps)
|
$: automationErrors = getAutomationErrors(enrichedApps)
|
||||||
|
$: isOwner = $auth.accountPortalAccess && $admin.cloud
|
||||||
|
|
||||||
|
const usersLimitLockAction = $licensing?.errUserLimit
|
||||||
|
? () => accountLockedModal.show()
|
||||||
|
: null
|
||||||
|
|
||||||
const enrichApps = (apps, user, sortBy) => {
|
const enrichApps = (apps, user, sortBy) => {
|
||||||
const enrichedApps = apps.map(app => ({
|
const enrichedApps = apps.map(app => ({
|
||||||
|
@ -189,6 +196,9 @@
|
||||||
creatingFromTemplate = true
|
creatingFromTemplate = true
|
||||||
createAppFromTemplateUrl(initInfo.init_template)
|
createAppFromTemplateUrl(initInfo.init_template)
|
||||||
}
|
}
|
||||||
|
if (usersLimitLockAction) {
|
||||||
|
usersLimitLockAction()
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error getting init info")
|
notifications.error("Error getting init info")
|
||||||
}
|
}
|
||||||
|
@ -230,20 +240,30 @@
|
||||||
<Layout noPadding gap="L">
|
<Layout noPadding gap="L">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<Button size="M" cta on:click={initiateAppCreation}>
|
<Button
|
||||||
|
size="M"
|
||||||
|
cta
|
||||||
|
on:click={usersLimitLockAction || initiateAppCreation}
|
||||||
|
>
|
||||||
Create new app
|
Create new app
|
||||||
</Button>
|
</Button>
|
||||||
{#if $apps?.length > 0}
|
{#if $apps?.length > 0}
|
||||||
<Button
|
<Button
|
||||||
size="M"
|
size="M"
|
||||||
secondary
|
secondary
|
||||||
on:click={$goto("/builder/portal/apps/templates")}
|
on:click={usersLimitLockAction ||
|
||||||
|
$goto("/builder/portal/apps/templates")}
|
||||||
>
|
>
|
||||||
View templates
|
View templates
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if !$apps?.length}
|
{#if !$apps?.length}
|
||||||
<Button size="L" quiet secondary on:click={initiateAppImport}>
|
<Button
|
||||||
|
size="L"
|
||||||
|
quiet
|
||||||
|
secondary
|
||||||
|
on:click={usersLimitLockAction || initiateAppImport}
|
||||||
|
>
|
||||||
Import app
|
Import app
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -267,7 +287,7 @@
|
||||||
|
|
||||||
<div class="app-table">
|
<div class="app-table">
|
||||||
{#each filteredApps as app (app.appId)}
|
{#each filteredApps as app (app.appId)}
|
||||||
<AppRow {app} />
|
<AppRow {app} lockedAction={usersLimitLockAction} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
@ -294,6 +314,11 @@
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<AppLimitModal bind:this={appLimitModal} />
|
<AppLimitModal bind:this={appLimitModal} />
|
||||||
|
<AccountLockedModal
|
||||||
|
bind:this={accountLockedModal}
|
||||||
|
onConfirm={() =>
|
||||||
|
isOwner ? $licensing.goToUpgradePage() : $licensing.goToPricingPage()}
|
||||||
|
/>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.title {
|
.title {
|
||||||
|
|
|
@ -107,8 +107,9 @@
|
||||||
useSampleData,
|
useSampleData,
|
||||||
isGoogle,
|
isGoogle,
|
||||||
}) => {
|
}) => {
|
||||||
|
let app
|
||||||
try {
|
try {
|
||||||
const app = await createApp(useSampleData)
|
app = await createApp(useSampleData)
|
||||||
|
|
||||||
let datasource
|
let datasource
|
||||||
if (datasourceConfig) {
|
if (datasourceConfig) {
|
||||||
|
@ -134,6 +135,17 @@
|
||||||
console.log(e)
|
console.log(e)
|
||||||
creationLoading = false
|
creationLoading = false
|
||||||
notifications.error("There was a problem creating your app")
|
notifications.error("There was a problem creating your app")
|
||||||
|
|
||||||
|
// Reset the store so that we don't send up stale headers
|
||||||
|
store.actions.reset()
|
||||||
|
|
||||||
|
// If we successfully created an app, delete it again so that we
|
||||||
|
// can try again once the error has been corrected.
|
||||||
|
// This also ensures onboarding can't be skipped by entering invalid
|
||||||
|
// data credentials.
|
||||||
|
if (app?.appId) {
|
||||||
|
await API.deleteApp(app.appId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -146,6 +158,7 @@
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<div class="full-width">
|
||||||
<SplitPage>
|
<SplitPage>
|
||||||
{#if stage === "name"}
|
{#if stage === "name"}
|
||||||
<NamePanel bind:name bind:url onNext={() => (stage = "data")} />
|
<NamePanel bind:name bind:url onNext={() => (stage = "data")} />
|
||||||
|
@ -163,7 +176,9 @@
|
||||||
{:else if stage === "data"}
|
{:else if stage === "data"}
|
||||||
<DataPanel onBack={() => (stage = "name")}>
|
<DataPanel onBack={() => (stage = "name")}>
|
||||||
<div class="dataButton">
|
<div class="dataButton">
|
||||||
<FancyButton on:click={() => handleCreateApp({ useSampleData: true })}>
|
<FancyButton
|
||||||
|
on:click={() => handleCreateApp({ useSampleData: true })}
|
||||||
|
>
|
||||||
<div class="dataButtonContent">
|
<div class="dataButtonContent">
|
||||||
<div class="dataButtonIcon">
|
<div class="dataButtonIcon">
|
||||||
<img
|
<img
|
||||||
|
@ -218,8 +233,12 @@
|
||||||
<ExampleApp {name} showData={stage !== "name"} />
|
<ExampleApp {name} showData={stage !== "name"} />
|
||||||
</div>
|
</div>
|
||||||
</SplitPage>
|
</SplitPage>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.full-width {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
.centered {
|
.centered {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
|
@ -176,7 +176,7 @@
|
||||||
<Heading>Backups</Heading>
|
<Heading>Backups</Heading>
|
||||||
{#if !$licensing.backupsEnabled}
|
{#if !$licensing.backupsEnabled}
|
||||||
<Tags>
|
<Tags>
|
||||||
<Tag icon="LockClosed">Pro plan</Tag>
|
<Tag icon="LockClosed">Premium</Tag>
|
||||||
</Tags>
|
</Tags>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import clientPackage from "@budibase/client/package.json"
|
|
||||||
import { processStringSync } from "@budibase/string-templates"
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
import { users, auth, apps, groups, overview } from "stores/portal"
|
import { users, auth, apps, groups, overview } from "stores/portal"
|
||||||
import { fetchData } from "@budibase/frontend-core"
|
import { fetchData } from "@budibase/frontend-core"
|
||||||
|
@ -40,7 +39,7 @@
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
$: updateAvailable = clientPackage.version !== $store.version
|
$: updateAvailable = $store.upgradableVersion !== $store.version
|
||||||
$: isPublished = app?.status === AppStatus.DEPLOYED
|
$: isPublished = app?.status === AppStatus.DEPLOYED
|
||||||
$: appEditorId = !app?.updatedBy ? $auth.user._id : app?.updatedBy
|
$: appEditorId = !app?.updatedBy ? $auth.user._id : app?.updatedBy
|
||||||
$: appEditorText = appEditor?.firstName || appEditor?.email
|
$: appEditorText = appEditor?.firstName || appEditor?.email
|
||||||
|
@ -172,8 +171,8 @@
|
||||||
<Heading size="XS">{$store.version}</Heading>
|
<Heading size="XS">{$store.version}</Heading>
|
||||||
{#if updateAvailable}
|
{#if updateAvailable}
|
||||||
<div class="version-status">
|
<div class="version-status">
|
||||||
New version <strong>{clientPackage.version}</strong> is available
|
New version <strong>{$store.upgradableVersion}</strong> is
|
||||||
-
|
available -
|
||||||
<Link
|
<Link
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
$goto("./version")
|
$goto("./version")
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import { Layout, Heading, Body, Divider, Button } from "@budibase/bbui"
|
import { Layout, Heading, Body, Divider, Button } from "@budibase/bbui"
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import clientPackage from "@budibase/client/package.json"
|
|
||||||
import VersionModal from "components/deploy/VersionModal.svelte"
|
import VersionModal from "components/deploy/VersionModal.svelte"
|
||||||
|
|
||||||
let versionModal
|
let versionModal
|
||||||
|
|
||||||
$: updateAvailable = clientPackage.version !== $store.version
|
$: updateAvailable = $store.upgradableVersion !== $store.version
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Layout noPadding>
|
<Layout noPadding>
|
||||||
|
@ -18,7 +17,7 @@
|
||||||
{#if updateAvailable}
|
{#if updateAvailable}
|
||||||
<Body>
|
<Body>
|
||||||
The app is currently using version <strong>{$store.version}</strong>
|
The app is currently using version <strong>{$store.version}</strong>
|
||||||
but version <strong>{clientPackage.version}</strong> is available.
|
but version <strong>{$store.upgradableVersion}</strong> is available.
|
||||||
<br />
|
<br />
|
||||||
Updates can contain new features, performance improvements and bug fixes.
|
Updates can contain new features, performance improvements and bug fixes.
|
||||||
</Body>
|
</Body>
|
||||||
|
|
|
@ -378,7 +378,7 @@
|
||||||
</div>
|
</div>
|
||||||
{#if !$licensing.enforceableSSO}
|
{#if !$licensing.enforceableSSO}
|
||||||
<Tags>
|
<Tags>
|
||||||
<Tag icon="LockClosed">Enterprise plan</Tag>
|
<Tag icon="LockClosed">Enterprise</Tag>
|
||||||
</Tags>
|
</Tags>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -213,7 +213,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
{#if isCloud && !brandingEnabled}
|
{#if isCloud && !brandingEnabled}
|
||||||
<Tags>
|
<Tags>
|
||||||
<Tag icon="LockClosed">Pro</Tag>
|
<Tag icon="LockClosed">Premium</Tag>
|
||||||
</Tags>
|
</Tags>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,47 +1,28 @@
|
||||||
<script>
|
<script>
|
||||||
import { url, goto } from "@roxi/routify"
|
|
||||||
import {
|
import {
|
||||||
Button,
|
ActionMenu,
|
||||||
Layout,
|
|
||||||
Heading,
|
Heading,
|
||||||
Icon,
|
Icon,
|
||||||
Popover,
|
Layout,
|
||||||
notifications,
|
|
||||||
Table,
|
|
||||||
ActionMenu,
|
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Modal,
|
Modal,
|
||||||
|
Table,
|
||||||
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import UserGroupPicker from "components/settings/UserGroupPicker.svelte"
|
import { goto, url } from "@roxi/routify"
|
||||||
import { createPaginationStore } from "helpers/pagination"
|
|
||||||
import { users, apps, groups, auth, features } from "stores/portal"
|
|
||||||
import { onMount, setContext } from "svelte"
|
|
||||||
import { roles } from "stores/backend"
|
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
import { Breadcrumb, Breadcrumbs } from "components/portal/page"
|
||||||
|
import { roles } from "stores/backend"
|
||||||
|
import { apps, auth, features, groups } from "stores/portal"
|
||||||
|
import { onMount, setContext } from "svelte"
|
||||||
|
import AppNameTableRenderer from "../users/_components/AppNameTableRenderer.svelte"
|
||||||
|
import AppRoleTableRenderer from "../users/_components/AppRoleTableRenderer.svelte"
|
||||||
import CreateEditGroupModal from "./_components/CreateEditGroupModal.svelte"
|
import CreateEditGroupModal from "./_components/CreateEditGroupModal.svelte"
|
||||||
import GroupIcon from "./_components/GroupIcon.svelte"
|
import GroupIcon from "./_components/GroupIcon.svelte"
|
||||||
import { Breadcrumbs, Breadcrumb } from "components/portal/page"
|
import GroupUsers from "./_components/GroupUsers.svelte"
|
||||||
import AppNameTableRenderer from "../users/_components/AppNameTableRenderer.svelte"
|
|
||||||
import RemoveUserTableRenderer from "./_components/RemoveUserTableRenderer.svelte"
|
|
||||||
import AppRoleTableRenderer from "../users/_components/AppRoleTableRenderer.svelte"
|
|
||||||
import ScimBanner from "../_components/SCIMBanner.svelte"
|
|
||||||
|
|
||||||
export let groupId
|
export let groupId
|
||||||
|
|
||||||
$: userSchema = {
|
|
||||||
email: {
|
|
||||||
width: "1fr",
|
|
||||||
},
|
|
||||||
...(readonly
|
|
||||||
? {}
|
|
||||||
: {
|
|
||||||
_id: {
|
|
||||||
displayName: "",
|
|
||||||
width: "auto",
|
|
||||||
borderLeft: true,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
const appSchema = {
|
const appSchema = {
|
||||||
name: {
|
name: {
|
||||||
width: "2fr",
|
width: "2fr",
|
||||||
|
@ -50,12 +31,6 @@
|
||||||
width: "1fr",
|
width: "1fr",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const customUserTableRenderers = [
|
|
||||||
{
|
|
||||||
column: "_id",
|
|
||||||
component: RemoveUserTableRenderer,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
const customAppTableRenderers = [
|
const customAppTableRenderers = [
|
||||||
{
|
{
|
||||||
column: "name",
|
column: "name",
|
||||||
|
@ -67,20 +42,12 @@
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
let popoverAnchor
|
|
||||||
let popover
|
|
||||||
let searchTerm = ""
|
|
||||||
let prevSearch = undefined
|
|
||||||
let pageInfo = createPaginationStore()
|
|
||||||
let loaded = false
|
let loaded = false
|
||||||
let editModal, deleteModal
|
let editModal, deleteModal
|
||||||
|
|
||||||
$: scimEnabled = $features.isScimEnabled
|
$: scimEnabled = $features.isScimEnabled
|
||||||
$: readonly = !$auth.isAdmin || scimEnabled
|
$: readonly = !$auth.isAdmin || scimEnabled
|
||||||
$: page = $pageInfo.page
|
|
||||||
$: fetchUsers(page, searchTerm)
|
|
||||||
$: group = $groups.find(x => x._id === groupId)
|
$: group = $groups.find(x => x._id === groupId)
|
||||||
$: filtered = $users.data
|
|
||||||
$: groupApps = $apps
|
$: groupApps = $apps
|
||||||
.filter(app =>
|
.filter(app =>
|
||||||
groups.actions
|
groups.actions
|
||||||
|
@ -97,25 +64,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchUsers(page, search) {
|
|
||||||
if ($pageInfo.loading) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// need to remove the page if they've started searching
|
|
||||||
if (search && !prevSearch) {
|
|
||||||
pageInfo.reset()
|
|
||||||
page = undefined
|
|
||||||
}
|
|
||||||
prevSearch = search
|
|
||||||
try {
|
|
||||||
pageInfo.loading()
|
|
||||||
await users.search({ page, email: search })
|
|
||||||
pageInfo.fetched($users.hasNextPage, $users.nextPage)
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error getting user list")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteGroup() {
|
async function deleteGroup() {
|
||||||
try {
|
try {
|
||||||
await groups.actions.delete(group)
|
await groups.actions.delete(group)
|
||||||
|
@ -130,21 +78,17 @@
|
||||||
try {
|
try {
|
||||||
await groups.actions.save(group)
|
await groups.actions.save(group)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error.message) {
|
||||||
|
notifications.error(error.message)
|
||||||
|
} else {
|
||||||
notifications.error(`Failed to save user group`)
|
notifications.error(`Failed to save user group`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeUser = async id => {
|
|
||||||
await groups.actions.removeUser(groupId, id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeApp = async app => {
|
const removeApp = async app => {
|
||||||
await groups.actions.removeApp(groupId, apps.getProdAppID(app.devId))
|
await groups.actions.removeApp(groupId, apps.getProdAppID(app.devId))
|
||||||
}
|
}
|
||||||
|
|
||||||
setContext("users", {
|
|
||||||
removeUser,
|
|
||||||
})
|
|
||||||
setContext("roles", {
|
setContext("roles", {
|
||||||
updateRole: () => {},
|
updateRole: () => {},
|
||||||
removeRole: removeApp,
|
removeRole: removeApp,
|
||||||
|
@ -186,41 +130,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Layout noPadding gap="S">
|
<Layout noPadding gap="S">
|
||||||
<div class="header">
|
<GroupUsers {groupId} />
|
||||||
<Heading size="S">Users</Heading>
|
|
||||||
{#if !scimEnabled}
|
|
||||||
<div bind:this={popoverAnchor}>
|
|
||||||
<Button disabled={readonly} on:click={popover.show()} cta
|
|
||||||
>Add user</Button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<ScimBanner />
|
|
||||||
{/if}
|
|
||||||
<Popover align="right" bind:this={popover} anchor={popoverAnchor}>
|
|
||||||
<UserGroupPicker
|
|
||||||
bind:searchTerm
|
|
||||||
labelKey="email"
|
|
||||||
selected={group.users?.map(user => user._id)}
|
|
||||||
list={$users.data}
|
|
||||||
on:select={e => groups.actions.addUser(groupId, e.detail)}
|
|
||||||
on:deselect={e => groups.actions.removeUser(groupId, e.detail)}
|
|
||||||
/>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Table
|
|
||||||
schema={userSchema}
|
|
||||||
data={group?.users}
|
|
||||||
allowEditRows={false}
|
|
||||||
customPlaceholder
|
|
||||||
customRenderers={customUserTableRenderers}
|
|
||||||
on:click={e => $goto(`../users/${e.detail._id}`)}
|
|
||||||
>
|
|
||||||
<div class="placeholder" slot="placeholder">
|
|
||||||
<Heading size="S">This user group doesn't have any users</Heading>
|
|
||||||
</div>
|
|
||||||
</Table>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<Layout noPadding gap="S">
|
<Layout noPadding gap="S">
|
||||||
|
|
|
@ -9,15 +9,23 @@
|
||||||
|
|
||||||
export let group
|
export let group
|
||||||
export let saveGroup
|
export let saveGroup
|
||||||
|
|
||||||
|
let nameError
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
onConfirm={() => saveGroup(group)}
|
onConfirm={() => {
|
||||||
|
if (!group.name?.trim()) {
|
||||||
|
nameError = "Group name cannot be empty"
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
saveGroup(group)
|
||||||
|
}}
|
||||||
size="M"
|
size="M"
|
||||||
title={group?._rev ? "Edit group" : "Create group"}
|
title={group?._rev ? "Edit group" : "Create group"}
|
||||||
confirmText="Save"
|
confirmText="Save"
|
||||||
>
|
>
|
||||||
<Input bind:value={group.name} label="Name" />
|
<Input bind:value={group.name} label="Name" error={nameError} />
|
||||||
<div class="modal-format">
|
<div class="modal-format">
|
||||||
<div class="modal-inner">
|
<div class="modal-inner">
|
||||||
<Body size="XS">Icon</Body>
|
<Body size="XS">Icon</Body>
|
||||||
|
|
|
@ -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) {
|
} catch (error) {
|
||||||
if (error.status === 400) {
|
if (error.status === 400) {
|
||||||
notifications.error(error.message)
|
notifications.error(error.message)
|
||||||
|
} else if (error.message) {
|
||||||
|
notifications.error(error.message)
|
||||||
} else {
|
} else {
|
||||||
notifications.error(`Failed to save group`)
|
notifications.error(`Failed to save group`)
|
||||||
}
|
}
|
||||||
|
@ -94,7 +96,7 @@
|
||||||
<Heading size="M">Groups</Heading>
|
<Heading size="M">Groups</Heading>
|
||||||
{#if !$licensing.groupsEnabled}
|
{#if !$licensing.groupsEnabled}
|
||||||
<Tags>
|
<Tags>
|
||||||
<Tag icon="LockClosed">Pro plan</Tag>
|
<Tag icon="LockClosed">Business</Tag>
|
||||||
</Tags>
|
</Tags>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -30,8 +30,8 @@
|
||||||
$: hasError = userData.find(x => x.error != null)
|
$: hasError = userData.find(x => x.error != null)
|
||||||
|
|
||||||
$: userCount = $licensing.userCount + userData.length
|
$: userCount = $licensing.userCount + userData.length
|
||||||
$: willReach = licensing.willReachUserLimit(userCount)
|
$: reached = licensing.usersLimitReached(userCount)
|
||||||
$: willExceed = licensing.willExceedUserLimit(userCount)
|
$: exceeded = licensing.usersLimitExceeded(userCount)
|
||||||
|
|
||||||
function removeInput(idx) {
|
function removeInput(idx) {
|
||||||
userData = userData.filter((e, i) => i !== idx)
|
userData = userData.filter((e, i) => i !== idx)
|
||||||
|
@ -87,7 +87,7 @@
|
||||||
confirmDisabled={disabled}
|
confirmDisabled={disabled}
|
||||||
cancelText="Cancel"
|
cancelText="Cancel"
|
||||||
showCloseIcon={false}
|
showCloseIcon={false}
|
||||||
disabled={hasError || !userData.length || willExceed}
|
disabled={hasError || !userData.length || exceeded}
|
||||||
>
|
>
|
||||||
<Layout noPadding gap="XS">
|
<Layout noPadding gap="XS">
|
||||||
<Label>Email address</Label>
|
<Label>Email address</Label>
|
||||||
|
@ -118,7 +118,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#if willReach}
|
{#if reached}
|
||||||
<div class="user-notification">
|
<div class="user-notification">
|
||||||
<Icon name="Info" />
|
<Icon name="Info" />
|
||||||
<span>
|
<span>
|
||||||
|
|
|
@ -25,10 +25,10 @@
|
||||||
$: invalidEmails = []
|
$: invalidEmails = []
|
||||||
|
|
||||||
$: userCount = $licensing.userCount + userEmails.length
|
$: userCount = $licensing.userCount + userEmails.length
|
||||||
$: willExceed = userCount > $licensing.userLimit
|
$: exceed = licensing.usersLimitExceeded(userCount)
|
||||||
|
|
||||||
$: importDisabled =
|
$: importDisabled =
|
||||||
!userEmails.length || !validEmails(userEmails) || !usersRole || willExceed
|
!userEmails.length || !validEmails(userEmails) || !usersRole || exceed
|
||||||
|
|
||||||
const validEmails = userEmails => {
|
const validEmails = userEmails => {
|
||||||
if ($admin.cloud && userEmails.length > MAX_USERS_UPLOAD_LIMIT) {
|
if ($admin.cloud && userEmails.length > MAX_USERS_UPLOAD_LIMIT) {
|
||||||
|
@ -93,7 +93,7 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if willExceed}
|
{#if exceed}
|
||||||
<div class="user-notification">
|
<div class="user-notification">
|
||||||
<Icon name="Info" />
|
<Icon name="Info" />
|
||||||
{capitalise($licensing.license.plan.type)} plan is limited to {$licensing.userLimit}
|
{capitalise($licensing.license.plan.type)} plan is limited to {$licensing.userLimit}
|
||||||
|
|
|
@ -88,6 +88,16 @@
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getPendingSchema = tblSchema => {
|
||||||
|
if (!tblSchema) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
let pendingSchema = JSON.parse(JSON.stringify(tblSchema))
|
||||||
|
pendingSchema.email.displayName = "Pending Invites"
|
||||||
|
return pendingSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
$: pendingSchema = getPendingSchema(schema)
|
||||||
$: userData = []
|
$: userData = []
|
||||||
$: inviteUsersResponse = { successful: [], unsuccessful: [] }
|
$: inviteUsersResponse = { successful: [], unsuccessful: [] }
|
||||||
$: {
|
$: {
|
||||||
|
@ -110,6 +120,24 @@
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
let invitesLoaded = false
|
||||||
|
let pendingInvites = []
|
||||||
|
let parsedInvites = []
|
||||||
|
|
||||||
|
const invitesToSchema = invites => {
|
||||||
|
return invites.map(invite => {
|
||||||
|
const { admin, builder, userGroups, apps } = invite.info
|
||||||
|
|
||||||
|
return {
|
||||||
|
email: invite.email,
|
||||||
|
builder,
|
||||||
|
admin,
|
||||||
|
userGroups: userGroups,
|
||||||
|
apps: apps ? [...new Set(Object.keys(apps))] : undefined,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
$: parsedInvites = invitesToSchema(pendingInvites)
|
||||||
|
|
||||||
const updateFetch = email => {
|
const updateFetch = email => {
|
||||||
fetch.update({
|
fetch.update({
|
||||||
|
@ -144,6 +172,7 @@
|
||||||
}))
|
}))
|
||||||
try {
|
try {
|
||||||
inviteUsersResponse = await users.invite(payload)
|
inviteUsersResponse = await users.invite(payload)
|
||||||
|
pendingInvites = await users.getInvites()
|
||||||
inviteConfirmationModal.show()
|
inviteConfirmationModal.show()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error inviting user")
|
notifications.error("Error inviting user")
|
||||||
|
@ -232,12 +261,13 @@
|
||||||
try {
|
try {
|
||||||
await groups.actions.init()
|
await groups.actions.init()
|
||||||
groupsLoaded = true
|
groupsLoaded = true
|
||||||
|
|
||||||
|
pendingInvites = await users.getInvites()
|
||||||
|
invitesLoaded = true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error fetching user group data")
|
notifications.error("Error fetching user group data")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
let staticUserLimit = $licensing.license.quotas.usage.static.users.value
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Layout noPadding gap="M">
|
<Layout noPadding gap="M">
|
||||||
|
@ -246,7 +276,7 @@
|
||||||
<Body>Add users and control who gets access to your published apps</Body>
|
<Body>Add users and control who gets access to your published apps</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
<Divider />
|
<Divider />
|
||||||
{#if $licensing.warnUserLimit}
|
{#if $licensing.errUserLimit}
|
||||||
<InlineAlert
|
<InlineAlert
|
||||||
type="error"
|
type="error"
|
||||||
onConfirm={() => {
|
onConfirm={() => {
|
||||||
|
@ -258,13 +288,9 @@
|
||||||
}}
|
}}
|
||||||
buttonText={isOwner ? "Upgrade" : "View plans"}
|
buttonText={isOwner ? "Upgrade" : "View plans"}
|
||||||
cta
|
cta
|
||||||
header={`Users will soon be limited to ${staticUserLimit}`}
|
header="Account de-activated"
|
||||||
message={`Our free plan is going to be limited to ${staticUserLimit} users in ${$licensing.userLimitDays}.
|
message="Due to the free plan user limit being exceeded, your account has been de-activated.
|
||||||
|
Upgrade your plan to re-activate your account."
|
||||||
This means any users exceeding the limit have been de-activated.
|
|
||||||
|
|
||||||
De-activated users will not able to access the builder or any published apps until you upgrade to one of our paid plans.
|
|
||||||
`}
|
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
|
@ -324,6 +350,15 @@
|
||||||
goToNextPage={fetch.nextPage}
|
goToNextPage={fetch.nextPage}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<Table
|
||||||
|
schema={pendingSchema}
|
||||||
|
data={parsedInvites}
|
||||||
|
allowEditColumns={false}
|
||||||
|
allowEditRows={false}
|
||||||
|
{customRenderers}
|
||||||
|
loading={!invitesLoaded}
|
||||||
|
allowClickRows={false}
|
||||||
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<Modal bind:this={createUserModal}>
|
<Modal bind:this={createUserModal}>
|
||||||
|
|
|
@ -25,6 +25,8 @@ export function createDatasourcesStore() {
|
||||||
store.update(state => ({
|
store.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
selectedDatasourceId: id,
|
selectedDatasourceId: id,
|
||||||
|
// Remove any possible schema error
|
||||||
|
schemaError: null,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ export function createGroupsStore() {
|
||||||
// on the backend anyway
|
// on the backend anyway
|
||||||
if (get(licensing).groupsEnabled) {
|
if (get(licensing).groupsEnabled) {
|
||||||
const groups = await API.getGroups()
|
const groups = await API.getGroups()
|
||||||
store.set(groups)
|
store.set(groups.data)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { auth, admin } from "stores/portal"
|
||||||
import { Constants } from "@budibase/frontend-core"
|
import { Constants } from "@budibase/frontend-core"
|
||||||
import { StripeStatus } from "components/portal/licensing/constants"
|
import { StripeStatus } from "components/portal/licensing/constants"
|
||||||
import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags"
|
import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags"
|
||||||
import dayjs from "dayjs"
|
import { PlanModel } from "@budibase/types"
|
||||||
|
|
||||||
const UNLIMITED = -1
|
const UNLIMITED = -1
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ export const createLicensingStore = () => {
|
||||||
const DEFAULT = {
|
const DEFAULT = {
|
||||||
// navigation
|
// navigation
|
||||||
goToUpgradePage: () => {},
|
goToUpgradePage: () => {},
|
||||||
|
goToPricingPage: () => {},
|
||||||
// the top level license
|
// the top level license
|
||||||
license: undefined,
|
license: undefined,
|
||||||
isFreePlan: true,
|
isFreePlan: true,
|
||||||
|
@ -37,29 +38,37 @@ export const createLicensingStore = () => {
|
||||||
// user limits
|
// user limits
|
||||||
userCount: undefined,
|
userCount: undefined,
|
||||||
userLimit: undefined,
|
userLimit: undefined,
|
||||||
userLimitDays: undefined,
|
|
||||||
userLimitReached: false,
|
userLimitReached: false,
|
||||||
warnUserLimit: false,
|
errUserLimit: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
const oneDayInMilliseconds = 86400000
|
const oneDayInMilliseconds = 86400000
|
||||||
|
|
||||||
const store = writable(DEFAULT)
|
const store = writable(DEFAULT)
|
||||||
|
|
||||||
function willReachUserLimit(userCount, userLimit) {
|
function usersLimitReached(userCount, userLimit) {
|
||||||
if (userLimit === UNLIMITED) {
|
if (userLimit === UNLIMITED) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return userCount >= userLimit
|
return userCount >= userLimit
|
||||||
}
|
}
|
||||||
|
|
||||||
function willExceedUserLimit(userCount, userLimit) {
|
function usersLimitExceeded(userCount, userLimit) {
|
||||||
if (userLimit === UNLIMITED) {
|
if (userLimit === UNLIMITED) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return userCount > userLimit
|
return userCount > userLimit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function isCloud() {
|
||||||
|
let adminStore = get(admin)
|
||||||
|
if (!adminStore.loaded) {
|
||||||
|
await admin.init()
|
||||||
|
adminStore = get(admin)
|
||||||
|
}
|
||||||
|
return adminStore.cloud
|
||||||
|
}
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
init: async () => {
|
init: async () => {
|
||||||
actions.setNavigation()
|
actions.setNavigation()
|
||||||
|
@ -71,10 +80,14 @@ export const createLicensingStore = () => {
|
||||||
const goToUpgradePage = () => {
|
const goToUpgradePage = () => {
|
||||||
window.location.href = upgradeUrl
|
window.location.href = upgradeUrl
|
||||||
}
|
}
|
||||||
|
const goToPricingPage = () => {
|
||||||
|
window.open("https://budibase.com/pricing/", "_blank")
|
||||||
|
}
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
goToUpgradePage,
|
goToUpgradePage,
|
||||||
|
goToPricingPage,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -128,15 +141,15 @@ export const createLicensingStore = () => {
|
||||||
quotaUsage,
|
quotaUsage,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
actions.setUsageMetrics()
|
await actions.setUsageMetrics()
|
||||||
},
|
},
|
||||||
willReachUserLimit: userCount => {
|
usersLimitReached: userCount => {
|
||||||
return willReachUserLimit(userCount, get(store).userLimit)
|
return usersLimitReached(userCount, get(store).userLimit)
|
||||||
},
|
},
|
||||||
willExceedUserLimit(userCount) {
|
usersLimitExceeded(userCount) {
|
||||||
return willExceedUserLimit(userCount, get(store).userLimit)
|
return usersLimitExceeded(userCount, get(store).userLimit)
|
||||||
},
|
},
|
||||||
setUsageMetrics: () => {
|
setUsageMetrics: async () => {
|
||||||
if (isEnabled(TENANT_FEATURE_FLAGS.LICENSING)) {
|
if (isEnabled(TENANT_FEATURE_FLAGS.LICENSING)) {
|
||||||
const usage = get(store).quotaUsage
|
const usage = get(store).quotaUsage
|
||||||
const license = get(auth).user.license
|
const license = get(auth).user.license
|
||||||
|
@ -198,11 +211,13 @@ export const createLicensingStore = () => {
|
||||||
const userQuota = license.quotas.usage.static.users
|
const userQuota = license.quotas.usage.static.users
|
||||||
const userLimit = userQuota?.value
|
const userLimit = userQuota?.value
|
||||||
const userCount = usage.usageQuota.users
|
const userCount = usage.usageQuota.users
|
||||||
const userLimitReached = willReachUserLimit(userCount, userLimit)
|
const userLimitReached = usersLimitReached(userCount, userLimit)
|
||||||
const userLimitExceeded = willExceedUserLimit(userCount, userLimit)
|
const userLimitExceeded = usersLimitExceeded(userCount, userLimit)
|
||||||
const days = dayjs(userQuota?.startDate).diff(dayjs(), "day")
|
const isCloudAccount = await isCloud()
|
||||||
const userLimitDays = days > 1 ? `${days} days` : "1 day"
|
const errUserLimit =
|
||||||
const warnUserLimit = userQuota?.startDate && userLimitExceeded
|
isCloudAccount &&
|
||||||
|
license.plan.model === PlanModel.PER_USER &&
|
||||||
|
userLimitExceeded
|
||||||
|
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
return {
|
return {
|
||||||
|
@ -217,9 +232,8 @@ export const createLicensingStore = () => {
|
||||||
// user limits
|
// user limits
|
||||||
userCount,
|
userCount,
|
||||||
userLimit,
|
userLimit,
|
||||||
userLimitDays,
|
|
||||||
userLimitReached,
|
userLimitReached,
|
||||||
warnUserLimit,
|
errUserLimit,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@ export function createUsersStore() {
|
||||||
inviteCode,
|
inviteCode,
|
||||||
password,
|
password,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName: !lastName?.trim() ? undefined : lastName,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,8 +114,10 @@ export function createUsersStore() {
|
||||||
const getUserRole = ({ admin, builder }) =>
|
const getUserRole = ({ admin, builder }) =>
|
||||||
admin?.global ? "admin" : builder?.global ? "developer" : "appUser"
|
admin?.global ? "admin" : builder?.global ? "developer" : "appUser"
|
||||||
|
|
||||||
const refreshUsage = fn => async args => {
|
const refreshUsage =
|
||||||
const response = await fn(args)
|
fn =>
|
||||||
|
async (...args) => {
|
||||||
|
const response = await fn(...args)
|
||||||
await licensing.setQuotaUsage()
|
await licensing.setQuotaUsage()
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
@ -133,7 +135,7 @@ export function createUsersStore() {
|
||||||
updateInvite,
|
updateInvite,
|
||||||
getUserCountByApp,
|
getUserCountByApp,
|
||||||
// any operation that adds or deletes users
|
// any operation that adds or deletes users
|
||||||
acceptInvite: refreshUsage(acceptInvite),
|
acceptInvite,
|
||||||
create: refreshUsage(create),
|
create: refreshUsage(create),
|
||||||
save: refreshUsage(save),
|
save: refreshUsage(save),
|
||||||
bulkDelete: refreshUsage(bulkDelete),
|
bulkDelete: refreshUsage(bulkDelete),
|
||||||
|
|
|
@ -4,18 +4,9 @@
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": "."
|
||||||
"paths": {
|
|
||||||
"@budibase/types": ["../types/src"],
|
|
||||||
"@budibase/backend-core": ["../backend-core/src"],
|
|
||||||
"@budibase/backend-core/*": ["../backend-core/*.js"]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"ts-node": {
|
"ts-node": {
|
||||||
"require": ["tsconfig-paths/register"]
|
"require": ["tsconfig-paths/register"]
|
||||||
},
|
}
|
||||||
"references": [
|
|
||||||
{ "path": "../types" },
|
|
||||||
{ "path": "../backend-core" },
|
|
||||||
]
|
|
||||||
}
|
}
|
|
@ -1,10 +1,10 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/cli",
|
"name": "@budibase/cli",
|
||||||
"version": "2.5.6-alpha.30",
|
"version": "0.0.1",
|
||||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||||
"main": "dist/index.js",
|
"main": "dist/src/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
"budi": "dist/index.js"
|
"budi": "dist/src/index.js"
|
||||||
},
|
},
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
|
@ -29,14 +29,14 @@
|
||||||
"outputPath": "build"
|
"outputPath": "build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/backend-core": "2.5.6-alpha.30",
|
"@budibase/backend-core": "0.0.1",
|
||||||
"@budibase/string-templates": "2.5.6-alpha.30",
|
"@budibase/string-templates": "0.0.1",
|
||||||
"@budibase/types": "2.5.6-alpha.30",
|
"@budibase/types": "0.0.1",
|
||||||
"axios": "0.21.2",
|
"axios": "0.21.2",
|
||||||
"chalk": "4.1.0",
|
"chalk": "4.1.0",
|
||||||
"cli-progress": "3.11.2",
|
"cli-progress": "3.11.2",
|
||||||
"commander": "7.1.0",
|
"commander": "7.1.0",
|
||||||
"docker-compose": "0.23.12",
|
"docker-compose": "0.24.0",
|
||||||
"dotenv": "16.0.1",
|
"dotenv": "16.0.1",
|
||||||
"download": "8.0.0",
|
"download": "8.0.0",
|
||||||
"find-free-port": "^2.0.0",
|
"find-free-port": "^2.0.0",
|
||||||
|
|
|
@ -1,19 +1,18 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import { logging } from "@budibase/backend-core"
|
process.env.DISABLE_PINO_LOGGER = "1"
|
||||||
logging.disableLogger()
|
|
||||||
import "./prebuilds"
|
import "./prebuilds"
|
||||||
import "./environment"
|
import "./environment"
|
||||||
import { env } from "@budibase/backend-core"
|
|
||||||
import { getCommands } from "./options"
|
import { getCommands } from "./options"
|
||||||
import { Command } from "commander"
|
import { Command } from "commander"
|
||||||
import { getHelpDescription } from "./utils"
|
import { getHelpDescription } from "./utils"
|
||||||
|
import { version } from "../package.json"
|
||||||
|
|
||||||
// add hosting config
|
// add hosting config
|
||||||
async function init() {
|
async function init() {
|
||||||
const program = new Command()
|
const program = new Command()
|
||||||
.addHelpCommand("help", getHelpDescription("Help with Budibase commands."))
|
.addHelpCommand("help", getHelpDescription("Help with Budibase commands."))
|
||||||
.helpOption(false)
|
.helpOption(false)
|
||||||
.version(env.VERSION)
|
.version(version)
|
||||||
// add commands
|
// add commands
|
||||||
for (let command of getCommands()) {
|
for (let command of getCommands()) {
|
||||||
command.configure(program)
|
command.configure(program)
|
||||||
|
|
|
@ -13,7 +13,7 @@ if (!process.argv[0].includes("node")) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkForBinaries() {
|
function checkForBinaries() {
|
||||||
const readDir = join(__filename, "..", "..", PREBUILDS, ARCH)
|
const readDir = join(__filename, "..", "..", "..", PREBUILDS, ARCH)
|
||||||
if (fs.existsSync(PREBUILD_DIR) || !fs.existsSync(readDir)) {
|
if (fs.existsSync(PREBUILD_DIR) || !fs.existsSync(readDir)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue