Merge branch 'develop' of github.com:Budibase/budibase into feature/multiselect-filters
This commit is contained in:
commit
ae9ae26bd4
|
@ -162,6 +162,7 @@
|
|||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "mslourens",
|
||||
"name": "Maurits Lourens",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1907152?v=4",
|
||||
|
|
|
@ -119,6 +119,8 @@ This job is responsible for deploying to our production, cloud kubernetes enviro
|
|||
|
||||
## Pro
|
||||
|
||||
| **NOTE**: When developing for both pro / budibase repositories, your branch names need to match, or else the correct pro doesn't get run within your CI job.
|
||||
|
||||
### Installing Pro
|
||||
|
||||
The pro package is always installed from source in our CI jobs.
|
||||
|
@ -132,7 +134,7 @@ This is done to prevent pro needing to be published prior to CI runs in budiabse
|
|||
- backend-core lives in the monorepo, so it can't be released independently to be used in pro
|
||||
- therefore the only option is to pull pro from source and release it as a part of the monorepo release, as if it were a mono package
|
||||
|
||||
The install is performed using the same steps as local development, via the `yarn bootstrap` command, see the [Contributing Guide#Pro](../CONTRIBUTING.md#pro)
|
||||
The install is performed using the same steps as local development, via the `yarn bootstrap` command, see the [Contributing Guide#Pro](../../docs/CONTRIBUTING.md#pro)
|
||||
|
||||
The branch to install pro from can vary depending on ref of the commit that triggered the budibase CI job. This is done to enable branches which have changes in both the monorepo and the pro repo to have their CI pass successfully.
|
||||
|
||||
|
|
|
@ -18,8 +18,9 @@ on:
|
|||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
# Posthog token used by ui at build time
|
||||
POSTHOG_TOKEN: phc_uDYOfnFt6wAbBAXkC6STjcrTpAFiWIhqgFcsC1UVO5F
|
||||
# Posthog token used by ui at build time
|
||||
# disable unless needed for testing
|
||||
# POSTHOG_TOKEN: phc_uDYOfnFt6wAbBAXkC6STjcrTpAFiWIhqgFcsC1UVO5F
|
||||
INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
|
||||
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
FEATURE_PREVIEW_URL: https://budirelease.live
|
||||
|
|
|
@ -3,24 +3,37 @@ name: Budibase Release Selfhost
|
|||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
BRANCH: ${{ github.event.pull_request.head.ref }}
|
||||
BASE_BRANCH: ${{ github.event.pull_request.base.ref}}
|
||||
|
||||
jobs:
|
||||
release:
|
||||
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:
|
||||
node-version: 14.x
|
||||
fetch_depth: 0
|
||||
|
||||
- name: Get the latest budibase release version
|
||||
id: version
|
||||
run: |
|
||||
release_version=$(cat lerna.json | jq -r '.version')
|
||||
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
||||
|
||||
- name: Tag and release Docker images (Self Host)
|
||||
run: |
|
||||
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
|
||||
|
||||
# Get latest release version
|
||||
release_version=$(cat lerna.json | jq -r '.version')
|
||||
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
||||
release_tag=v$release_version
|
||||
release_tag=v${{ env.RELEASE_VERSION }}
|
||||
|
||||
# Pull apps and worker images
|
||||
docker pull budibase/apps:$release_tag
|
||||
|
@ -40,13 +53,15 @@ jobs:
|
|||
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
|
||||
SELFHOST_TAG: latest
|
||||
|
||||
- name: Build CLI executables
|
||||
|
||||
- name: Install Pro
|
||||
run: yarn install:pro $BRANCH $BASE_BRANCH
|
||||
|
||||
- name: Bootstrap and build (CLI)
|
||||
run: |
|
||||
pushd packages/cli
|
||||
yarn
|
||||
yarn bootstrap
|
||||
yarn build
|
||||
popd
|
||||
|
||||
- name: Build OpenAPI spec
|
||||
run: |
|
||||
|
@ -93,4 +108,4 @@ jobs:
|
|||
with:
|
||||
webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }}
|
||||
content: "Self Host Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Self Host."
|
||||
embed-title: ${{ env.RELEASE_VERSION }}
|
||||
embed-title: ${{ env.RELEASE_VERSION }}
|
||||
|
|
|
@ -29,7 +29,7 @@ on:
|
|||
|
||||
env:
|
||||
# Posthog token used by ui at build time
|
||||
POSTHOG_TOKEN: phc_fg5I3nDOf6oJVMHSaycEhpPdlgS8rzXG2r6F2IpxCHS
|
||||
POSTHOG_TOKEN: phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU
|
||||
INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
|
|
|
@ -169,7 +169,7 @@ If you have a question or would like to talk with other Budibase users and join
|
|||
|
||||
## ❗ Code of conduct
|
||||
|
||||
Budibase is dedicated to providing a welcoming, diverse, and harrassment-free experience for everyone. We expect everyone in the Budibase community to abide by our [**Code of Conduct**](https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md). Please read it.
|
||||
Budibase is dedicated to providing a welcoming, diverse, and harrassment-free experience for everyone. We expect everyone in the Budibase community to abide by our [**Code of Conduct**](https://github.com/Budibase/budibase/blob/HEAD/docs/CODE_OF_CONDUCT.md). Please read it.
|
||||
<br />
|
||||
|
||||
|
||||
|
|
|
@ -91,7 +91,7 @@ globals:
|
|||
budibaseEnv: PRODUCTION
|
||||
enableAnalytics: "1"
|
||||
sentryDSN: ""
|
||||
posthogToken: "phc_fg5I3nDOf6oJVMHSaycEhpPdlgS8rzXG2r6F2IpxCHS"
|
||||
posthogToken: "phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU"
|
||||
logLevel: info
|
||||
selfHosted: "1" # set to 0 for budibase cloud environment, set to 1 for self-hosted setup
|
||||
multiTenancy: "0" # set to 0 to disable multiple orgs, set to 1 to enable multiple orgs
|
||||
|
|
|
@ -4,10 +4,10 @@ From opening a bug report to creating a pull request: every contribution is appr
|
|||
|
||||
## Table of contents
|
||||
|
||||
- [Quick start](#quick-start)
|
||||
- [Status](#status)
|
||||
- [What's included](#whats-included)
|
||||
- [Bugs and feature requests](#bugs-and-feature-requests)
|
||||
- [Where to start](#not-sure-where-to-start)
|
||||
- [Contributor Licence Agreement](#contributor-license-agreement-cla)
|
||||
- [Glossary of Terms](#glossary-of-terms)
|
||||
- [Contributing to Budibase](#contributing-to-budibase)
|
||||
|
||||
|
||||
## Not Sure Where to Start?
|
||||
|
@ -32,6 +32,9 @@ All contributors must sign an [Individual Contributor License Agreement](https:/
|
|||
|
||||
If contributing on behalf of your company, your company must sign a [Corporate Contributor License Agreement](https://github.com/budibase/budibase/blob/next/.github/cla/corporate-cla.md). If so, please contact us via community@budibase.com.
|
||||
|
||||
If for any reason, your first contribution is in a PR created by other contributor, please just add a comment to the PR
|
||||
with the following text to agree our CLA: "I have read the CLA Document and I hereby sign the CLA".
|
||||
|
||||
## Glossary of Terms
|
||||
|
||||
To understand the budibase API, it can be helpful to understand the top level entities that make up Budibase.
|
||||
|
@ -162,7 +165,10 @@ When you are running locally, budibase stores data on disk using docker volumes.
|
|||
|
||||
### Development Modes
|
||||
|
||||
A combination of environment variables controls the mode budibase runs in.
|
||||
A combination of environment variables controls the mode budibase runs in.
|
||||
|
||||
| **NOTE**: You need to clean your browser cookies when you change between different modes.
|
||||
|
||||
Yarn commands can be used to mimic the different modes as described in the sections below:
|
||||
|
||||
#### Self Hosted
|
||||
|
@ -189,7 +195,7 @@ To enable this mode, use:
|
|||
yarn mode:account
|
||||
```
|
||||
### CI
|
||||
An overview of the CI pipelines can be found [here](./workflows/README.md)
|
||||
An overview of the CI pipelines can be found [here](../.github/workflows/README.md)
|
||||
|
||||
### Pro
|
||||
|
||||
|
|
|
@ -4,6 +4,11 @@
|
|||
|
||||
Install instructions [here](https://brew.sh/)
|
||||
|
||||
| **NOTE**: If you are working on a M1 Apple Silicon which is running Z shell, you could need to add
|
||||
`eval $(/opt/homebrew/bin/brew shellenv)` line to your `.zshrc`. This will make your zsh to find the apps you install
|
||||
through brew.
|
||||
|
||||
|
||||
### Install Node
|
||||
|
||||
Budibase requires a recent version of node (14+):
|
||||
|
@ -51,4 +56,7 @@ So this command will actually run the application in dev mode. It creates .env f
|
|||
|
||||
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
|
||||
|
||||
| **NOTE**: If you are working on a M1 Apple Silicon, you will need to uncomment `# platform: linux/amd64` line in
|
||||
[hosting/docker-compose-dev.yaml](../hosting/docker-compose.dev.yaml)
|
|
@ -76,6 +76,8 @@ services:
|
|||
- "${MAIN_PORT}:10000"
|
||||
container_name: bbproxy
|
||||
image: budibase/proxy
|
||||
environment:
|
||||
- PROXY_RATE_LIMIT_WEBHOOKS_PER_SECOND=10
|
||||
depends_on:
|
||||
- minio-service
|
||||
- worker-service
|
||||
|
|
|
@ -9,7 +9,11 @@ events {
|
|||
}
|
||||
|
||||
http {
|
||||
# rate limiting
|
||||
limit_req_status 429;
|
||||
limit_req_zone $binary_remote_addr zone=ratelimit:10m rate=20r/s;
|
||||
limit_req_zone $binary_remote_addr zone=webhooks:10m rate=${PROXY_RATE_LIMIT_WEBHOOKS_PER_SECOND}r/s;
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
proxy_set_header Host $host;
|
||||
|
@ -126,6 +130,25 @@ http {
|
|||
proxy_pass http://$apps:4002;
|
||||
}
|
||||
|
||||
location /api/webhooks/ {
|
||||
# calls to webhooks are rate limited
|
||||
limit_req zone=webhooks nodelay;
|
||||
|
||||
# Rest of configuration copied from /api/ location above
|
||||
# 120s timeout on API requests
|
||||
proxy_read_timeout 120s;
|
||||
proxy_connect_timeout 120s;
|
||||
proxy_send_timeout 120s;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
proxy_pass http://$apps:4002;
|
||||
}
|
||||
|
||||
location /db/ {
|
||||
proxy_pass http://$couchdb:5984;
|
||||
rewrite ^/db/(.*)$ /$1 break;
|
||||
|
|
|
@ -1,3 +1,13 @@
|
|||
FROM nginx:latest
|
||||
COPY .generated-nginx.prod.conf /etc/nginx/nginx.conf
|
||||
COPY error.html /usr/share/nginx/html/error.html
|
||||
|
||||
# nginx.conf
|
||||
# use the default nginx behaviour for *.template files which are processed with envsubst
|
||||
# override the output dir to output directly to /etc/nginx instead of /etc/nginx/conf.d
|
||||
ENV NGINX_ENVSUBST_OUTPUT_DIR=/etc/nginx
|
||||
COPY .generated-nginx.prod.conf /etc/nginx/templates/nginx.conf.template
|
||||
|
||||
# Error handling
|
||||
COPY error.html /usr/share/nginx/html/error.html
|
||||
|
||||
# Default environment
|
||||
ENV PROXY_RATE_LIMIT_WEBHOOKS_PER_SECOND=10
|
|
@ -3,15 +3,14 @@
|
|||
echo ${TARGETBUILD} > /buildtarget.txt
|
||||
if [[ "${TARGETBUILD}" = "aas" ]]; then
|
||||
# Azure AppService uses /home for persisent data & SSH on port 2222
|
||||
mkdir -p /home/{search,minio,couch}
|
||||
mkdir -p /home/couch/{dbs,views}
|
||||
chown -R couchdb:couchdb /home/couch/
|
||||
DATA_DIR=/home
|
||||
mkdir -p $DATA_DIR/{search,minio,couchdb}
|
||||
mkdir -p $DATA_DIR/couchdb/{dbs,views}
|
||||
chown -R couchdb:couchdb $DATA_DIR/couchdb/
|
||||
apt update
|
||||
apt-get install -y openssh-server
|
||||
sed -i 's#dir=/opt/couchdb/data/search#dir=/home/search#' /opt/clouseau/clouseau.ini
|
||||
sed -i 's#/minio/minio server /minio &#/minio/minio server /home/minio &#' /runner.sh
|
||||
sed -i 's#database_dir = ./data#database_dir = /home/couch/dbs#' /opt/couchdb/etc/default.ini
|
||||
sed -i 's#view_index_dir = ./data#view_index_dir = /home/couch/views#' /opt/couchdb/etc/default.ini
|
||||
sed -i "s/#Port 22/Port 2222/" /etc/ssh/sshd_config
|
||||
/etc/init.d/ssh restart
|
||||
fi
|
||||
|
||||
sed -i 's#DATA_DIR#$DATA_DIR#' /opt/clouseau/clouseau.ini /opt/couchdb/etc/local.ini
|
||||
|
|
|
@ -20,10 +20,10 @@ RUN node /pinVersions.js && yarn && yarn build && /cleanup.sh
|
|||
|
||||
FROM couchdb:3.2.1
|
||||
# TARGETARCH can be amd64 or arm e.g. docker build --build-arg TARGETARCH=amd64
|
||||
ARG TARGETARCH amd64
|
||||
ARG TARGETARCH=amd64
|
||||
#TARGETBUILD can be set to single (for single docker image) or aas (for azure app service)
|
||||
# e.g. docker build --build-arg TARGETBUILD=aas ....
|
||||
ARG TARGETBUILD single
|
||||
ARG TARGETBUILD=single
|
||||
ENV TARGETBUILD $TARGETBUILD
|
||||
|
||||
COPY --from=build /app /app
|
||||
|
@ -35,9 +35,10 @@ ENV \
|
|||
BUDIBASE_ENVIRONMENT=PRODUCTION \
|
||||
CLUSTER_PORT=80 \
|
||||
# CUSTOM_DOMAIN=budi001.custom.com \
|
||||
DATA_DIR=/data \
|
||||
DEPLOYMENT_ENVIRONMENT=docker \
|
||||
MINIO_URL=http://localhost:9000 \
|
||||
POSTHOG_TOKEN=phc_fg5I3nDOf6oJVMHSaycEhpPdlgS8rzXG2r6F2IpxCHS \
|
||||
POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU \
|
||||
REDIS_URL=localhost:6379 \
|
||||
SELF_HOSTED=1 \
|
||||
TARGETBUILD=$TARGETBUILD \
|
||||
|
@ -114,6 +115,7 @@ RUN chmod +x ./healthcheck.sh
|
|||
ADD hosting/scripts/build-target-paths.sh .
|
||||
RUN chmod +x ./build-target-paths.sh
|
||||
|
||||
# Script below sets the path for storing data based on $DATA_DIR
|
||||
# For Azure App Service install SSH & point data locations to /home
|
||||
RUN /build-target-paths.sh
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ name=clouseau@127.0.0.1
|
|||
cookie=monster
|
||||
|
||||
; the path where you would like to store the search index files
|
||||
dir=/data/search
|
||||
dir=DATA_DIR/search
|
||||
|
||||
; the number of search indexes that can be open simultaneously
|
||||
max_indexes_open=500
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
; CouchDB Configuration Settings
|
||||
|
||||
[couchdb]
|
||||
database_dir = /data/couch/dbs
|
||||
view_index_dir = /data/couch/views
|
||||
database_dir = DATA_DIR/couch/dbs
|
||||
view_index_dir = DATA_DIR/couch/views
|
||||
|
|
|
@ -1,7 +1,16 @@
|
|||
#!/bin/bash
|
||||
declare -a ENV_VARS=("COUCHDB_USER" "COUCHDB_PASSWORD" "MINIO_ACCESS_KEY" "MINIO_SECRET_KEY" "INTERNAL_API_KEY" "JWT_SECRET" "REDIS_PASSWORD")
|
||||
if [ -f "/data/.env" ]; then
|
||||
export $(cat /data/.env | xargs)
|
||||
declare -a ENV_VARS=("COUCHDB_USER" "COUCHDB_PASSWORD" "DATA_DIR" "MINIO_ACCESS_KEY" "MINIO_SECRET_KEY" "INTERNAL_API_KEY" "JWT_SECRET" "REDIS_PASSWORD")
|
||||
|
||||
# Azure App Service customisations
|
||||
if [[ "${TARGETBUILD}" = "aas" ]]; then
|
||||
DATA_DIR=/home
|
||||
/etc/init.d/ssh start
|
||||
else
|
||||
DATA_DIR=${DATA_DIR:-/data}
|
||||
fi
|
||||
|
||||
if [ -f "${DATA_DIR}/.env" ]; then
|
||||
export $(cat ${DATA_DIR}/.env | xargs)
|
||||
fi
|
||||
# first randomise any unset environment variables
|
||||
for ENV_VAR in "${ENV_VARS[@]}"
|
||||
|
@ -14,21 +23,26 @@ done
|
|||
if [[ -z "${COUCH_DB_URL}" ]]; then
|
||||
export COUCH_DB_URL=http://$COUCHDB_USER:$COUCHDB_PASSWORD@localhost:5984
|
||||
fi
|
||||
if [ ! -f "/data/.env" ]; then
|
||||
touch /data/.env
|
||||
if [ ! -f "${DATA_DIR}/.env" ]; then
|
||||
touch ${DATA_DIR}/.env
|
||||
for ENV_VAR in "${ENV_VARS[@]}"
|
||||
do
|
||||
temp=$(eval "echo \$$ENV_VAR")
|
||||
echo "$ENV_VAR=$temp" >> /data/.env
|
||||
echo "$ENV_VAR=$temp" >> ${DATA_DIR}/.env
|
||||
done
|
||||
echo "COUCH_DB_URL=${COUCH_DB_URL}" >> ${DATA_DIR}/.env
|
||||
fi
|
||||
|
||||
export COUCH_DB_URL=http://$COUCHDB_USER:$COUCHDB_PASSWORD@localhost:5984
|
||||
|
||||
# make these directories in runner, incase of mount
|
||||
mkdir -p /data/couch/{dbs,views} /home/couch/{dbs,views}
|
||||
chown -R couchdb:couchdb /data/couch /home/couch
|
||||
mkdir -p ${DATA_DIR}/couchdb/{dbs,views}
|
||||
mkdir -p ${DATA_DIR}/minio
|
||||
mkdir -p ${DATA_DIR}/search
|
||||
chown -R couchdb:couchdb ${DATA_DIR}/couchdb
|
||||
redis-server --requirepass $REDIS_PASSWORD &
|
||||
/opt/clouseau/bin/clouseau &
|
||||
/minio/minio server /data/minio &
|
||||
/minio/minio server ${DATA_DIR}/minio &
|
||||
/docker-entrypoint.sh /opt/couchdb/bin/couchdb &
|
||||
/etc/init.d/nginx restart
|
||||
if [[ ! -z "${CUSTOM_DOMAIN}" ]]; then
|
||||
|
|
|
@ -8,10 +8,11 @@
|
|||
</h1>
|
||||
|
||||
<h3 align="center">
|
||||
Construye herramientas empresariales personalizadas en cuestión de minutos y en su propia infraestructura.
|
||||
Construye herramientas empresariales personalizadas en cuestión de minutos y en tu propia infraestructura.
|
||||
</h3>
|
||||
<p align="center">
|
||||
Budibase es una plataforma de código bajo de código abierto, que ayuda a desarrolladores y profesionales de TI a crear, automatizar y enviar aplicaciones empresariales personalizadas en cuestión de minutos y en su propia infraestructura
|
||||
Budibase es una plataforma low code de código abierto, que ayuda a desarrolladores y profesionales de TI a crear y
|
||||
automatizar aplicaciones personalizadas en cuestión de minutos
|
||||
</p>
|
||||
|
||||
<h3 align="center">
|
||||
|
@ -20,7 +21,7 @@
|
|||
|
||||
|
||||
<p align="center">
|
||||
<img src="https://i.imgur.com/tPQHruf.png">
|
||||
<img alt="Budibase design ui" src="https://res.cloudinary.com/daog6scxm/image/upload/v1633524049/ui/design-ui-wide-mobile_gdaveq.jpg">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
|
@ -30,9 +31,6 @@
|
|||
<a href="https://github.com/Budibase/budibase/releases">
|
||||
<img alt="GitHub release (latest by date)" src="https://img.shields.io/github/v/release/Budibase/budibase">
|
||||
</a>
|
||||
<a href="https://discord.gg/rCYayfe">
|
||||
<img alt="Discord" src="https://img.shields.io/discord/733030666647765003">
|
||||
</a>
|
||||
<a href="https://twitter.com/intent/follow?screen_name=budibase">
|
||||
<img src="https://img.shields.io/twitter/follow/budibase?style=social" alt="Follow @budibase" />
|
||||
</a>
|
||||
|
@ -43,130 +41,213 @@
|
|||
</p>
|
||||
|
||||
<h3 align="center">
|
||||
<a href="https://portal.budi.live/signup">Sign-up</a>
|
||||
<a href="https://account.budibase.app/register">Comenzar con Budibase en la nube</a>
|
||||
<span> · </span>
|
||||
<a href="https://docs.budibase.com">Docs</a>
|
||||
<a href="https://docs.budibase.com/docs/hosting-methods">Comenzar con Docker, K8s, DO</a>
|
||||
<span> · </span>
|
||||
<a href="https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas">Feature request</a>
|
||||
<a href="https://docs.budibase.com/docs">Documentaciones</a>
|
||||
<span> · </span>
|
||||
<a href="https://github.com/Budibase/budibase/issues">Report a bug</a>
|
||||
<a href="https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas">Pedir una funcionalidad</a>
|
||||
<span> · </span>
|
||||
Support: <a href="https://github.com/Budibase/budibase/discussions">Discussions</a>
|
||||
<span> & </span>
|
||||
<a href="https://discord.gg/rCYayfe">Discord</a>
|
||||
<a href="https://github.com/Budibase/budibase/issues">Reportar un error</a>
|
||||
<span> · </span>
|
||||
Support: <a href="https://github.com/Budibase/budibase/discussions">Comunidad</a>
|
||||
</h3>
|
||||
|
||||
<br /><br />
|
||||
## ✨ Caracteristicas
|
||||
|
||||
## ✨ Features
|
||||
When other platforms chose the closed source route, we decided to go open source. When other platforms chose cloud builders, we decided a local builder offered the better developer experience. We like to do things differently at Budibase.
|
||||
### Construir aplicaciones reales
|
||||
Con Budibase podras construir aplicaciones de pagina unica de gran rendimiento. Ademas, puedes hacerlas con un diseño
|
||||
adaptativo para darles a tus usuarios una gran experiencia.
|
||||
<br /><br />
|
||||
|
||||
- **Build and ship real software.** Unlike other platforms, with Budibase you build and ship single page applications. Budibase applications have performance baked in and can be designed responsively, providing your users with a great experience.
|
||||
### Codigo abierto y ampliable
|
||||
Budibase es de codigo abierto con licencia GPL v3. Puedes ampliarlo o modificarlo para adaptarlo a tus necesidades y preferencias.
|
||||
|
||||
- **Open source and extensable.** Budibase is open-source. The builder is licensed AGPL v3, the server is GPL v3, and the client is MPL. This should fill you with confidence that Budibase will always be around. You can also code against Budibase or fork it and make changes as you please, providing a developer-friendly experience.
|
||||
De esta manera proveemos una buena experiencia para el desarrollador asi como establecemos la confianza de que Budibase siempre estara funcional.
|
||||
<br /><br />
|
||||
|
||||
- **Load data or start from scratch.** Budibase pulls in data from multiple sources, including MongoDB, CouchDB, PostgreSQL, mySQL, Airtable, Google Sheets, S3, DyanmoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no data sources. [Request new data sources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||
### Cargar informacion o empezar desde cero
|
||||
Budibase permite importar datos desde multiples fuentes, entre las que estan incluidas: MondoDB, CouchDB, PostgreSQL, MySQL,
|
||||
Airtable, S3, DynamoDB o API REST.
|
||||
|
||||
- **Design and build apps with powerful pre-made components.** Budibase comes out of the box with beautifully designed, powerful components which you can use like building blocks to build your UI. We also expose a lot of your favourite CSS styling options so you can go that extra creative mile. [Request new components](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||
|
||||
- **Automate processes, integrate with other tools, and connect to webhooks.** Save time by automating manual processes and workflows. From connecting to webhooks, to automating emails, simply tell Budibase what to do and let it work for you. You can easily [create new automations for Budibase here](https://github.com/Budibase/automations) or [request new integrations here](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||
|
||||
- **Cloud hosting and self-hosting.** Users can self-host (see below), or host their apps with Budibase. Currently, our cloud hosting offering is limited to the free tier but we aim to change this in the future. For heavy usage, we advise users to self-host.
|
||||
O si lo prefieres, con Budibase puedes empezar desde cero y construir tus propias aplicaciones
|
||||
sin necesidad de herramientas externas.
|
||||
[Sugerir fuente de datos](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||
|
||||
<p align="center">
|
||||
<img alt="Budibase design ui" src="https://imgur.com/v8m6v3q.png">
|
||||
<img alt="Budibase data" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970242/Out%20of%20beta%20launch/data_n1tlhf.png">
|
||||
</p>
|
||||
<br /><br />
|
||||
|
||||
### Diseña y construye aplicaciones con componentes profesionales prediseñados
|
||||
|
||||
## ⌛ Status
|
||||
- [x] Alpha: We are demoing Budibase to users and receiving feedback
|
||||
- [x] Private Beta: We are testing Budibase with a closed set of customers
|
||||
- [x] Public Beta: Anyone can [sign-up and use Budibase](https://portal.budi.live/signup).
|
||||
- [ ] Official Launch
|
||||
Budibase incorpora componentes profesionales prediseñados que podras usar de manera facil e intuitiva
|
||||
como bloques de construccion para la interfaz de tu aplicacion.
|
||||
|
||||
Watch "releases" of this repo to get notified of major updates, and give the star button a click whilst you're there.
|
||||
Tambien mostramos gran parte del CSS para que puedas adaptar los componentes a tus diseños.
|
||||
[Sugerir componente](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||
|
||||
<p align="center">
|
||||
<img src="https://i.imgur.com/cJpgqm8.png">
|
||||
<img alt="Budibase design" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970243/Out%20of%20beta%20launch/design-like-a-pro_qhlfeu.gif">
|
||||
</p>
|
||||
<br /><br />
|
||||
|
||||
### Stargazers over time
|
||||
### Procesos automatizados, integra tu aplicacion con otras herramientas y conectala a eventos webhook
|
||||
|
||||
Ahorra tiempo automatizando flujos de trabajo y procesos manuales. Podras desde conectar eventos webhook hasta automatizar emails,
|
||||
simplemente dile a Budibase que hacer y deja que el haga el trabajo por ti.
|
||||
[Crear nuevos procesos automatizados](https://github.com/Budibase/automations) o [Sugerir proceso automatizado](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||
|
||||
<p align="center">
|
||||
<img alt="Budibase automations" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970486/Out%20of%20beta%20launch/automation_riro7u.png">
|
||||
</p>
|
||||
<br /><br />
|
||||
|
||||
### Tus herramientas favoritas
|
||||
|
||||
Budibase integra un gran numero de herramientas que te permitiran construir tus aplicaciones ajustandose a tus preferencias.
|
||||
|
||||
<p align="center">
|
||||
<img alt="Budibase integrations" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970242/Out%20of%20beta%20launch/integrations_kc7dqt.png">
|
||||
</p>
|
||||
<br /><br />
|
||||
|
||||
### Un paraiso para administradores
|
||||
|
||||
Puedes albergar Budibase en tu propia infraestructura y gestionar globalmente usuarios, incorporaciones, SMTP, aplicaciones,
|
||||
grupos, diseños de temas, etc.
|
||||
|
||||
Tambien puedes gestionar los usuarios y grupos, o delegar en personas asignadas para ello, desde nuestra aplicacion sin
|
||||
mucho esfuerzo.
|
||||
|
||||
Budibase is made to scale. With Budibase, you can self-host on your own infrastructure and globally manage users, onboarding, SMTP, apps, groups, theming and more. You can also provide users/groups with an app portal and disseminate user-management to the group manager.
|
||||
|
||||
- Video Promocional: https://youtu.be/xoljVpty_Kw
|
||||
|
||||
<br />
|
||||
|
||||
---
|
||||
|
||||
<br />
|
||||
|
||||
|
||||
## Budibase API Publica
|
||||
|
||||
Como todo lo que construimos en Budibase, nuestra nueva API publica es facil de usar, flexible e introduce nueva ampliacion
|
||||
del sistema. Budibase API ofrece:
|
||||
- Uso de Budibase como backend
|
||||
- Interoperabilidad
|
||||
|
||||
#### Documentacion
|
||||
|
||||
Puedes aprender mas acerca de Budibase API en los siguientes documentos:
|
||||
- [Documentacion general](https://docs.budibase.com/docs/public-api) : Como optener tu clave para la API, usar Insomnia y Postman
|
||||
- [API Interactiva](https://docs.budibase.com/reference/post_applications) : Aprende como trabajar con la API
|
||||
|
||||
#### Guias
|
||||
|
||||
- [Construye una aplicacion con Budibase y Next.js](https://budibase.com/blog/building-a-crud-app-with-budibase-and-next.js/)
|
||||
|
||||
<p align="center">
|
||||
<img alt="Budibase data" src="https://res.cloudinary.com/daog6scxm/image/upload/v1647858558/Feb%20release/Start_building_with_Budibase_s_API_3_rhlzhv.png">
|
||||
</p>
|
||||
<br /><br />
|
||||
|
||||
<br /><br /><br />
|
||||
|
||||
## 🏁 Comenzar con Budibase
|
||||
|
||||
Puedes alojar Budibase en tu propia infraestructura con Docker, Kubernetes o Digital Ocean; o usa Budibase en la nube si
|
||||
quieres empezar a crear tus aplicaciones rapidamente y sin ningun tipo de preocupacion.
|
||||
|
||||
### [Comenzar con Budibase self-hosting](https://docs.budibase.com/docs/hosting-methods)
|
||||
|
||||
- [Docker - single ARM compatible image](https://docs.budibase.com/docs/docker)
|
||||
- [Docker Compose](https://docs.budibase.com/docs/docker-compose)
|
||||
- [Kubernetes](https://docs.budibase.com/docs/kubernetes-k8s)
|
||||
- [Digital Ocean](https://docs.budibase.com/docs/digitalocean)
|
||||
- [Portainer](https://docs.budibase.com/docs/portainer)
|
||||
|
||||
|
||||
### [Comenzar con Budibase en la nube](https://budibase.com)
|
||||
|
||||
<br /><br />
|
||||
|
||||
## 🎓 Aprende a usar Budibase
|
||||
|
||||
Aqui tienes la [documentacion de Budibase](https://docs.budibase.com/docs).
|
||||
<br />
|
||||
|
||||
|
||||
<br /><br />
|
||||
|
||||
## 💬 Comunidad
|
||||
|
||||
Te invitamos a que te unas a nuestra comunidad de Budibase, alli podras hacer las preguntas que quieras, ayudar a otras
|
||||
personas o tener una charla entretenida con otros usuarios de Budibase.
|
||||
[Acceder a la comunidad de Budibase](https://github.com/Budibase/budibase/discussions)
|
||||
<br /><br /><br />
|
||||
|
||||
|
||||
## ❗ Codigo de conducta
|
||||
|
||||
Budibase presta especial atencion en acoger a personas de toda diversidad y ofrecer un entorno de respeto mutuo. Asi mismo
|
||||
esperamos lo mismo de nuestra comunidad, por favor lee el
|
||||
[**Codigo de conducta**](https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md).
|
||||
<br />
|
||||
|
||||
<br /><br />
|
||||
|
||||
|
||||
## 🙌 Contribuir en Budibase
|
||||
|
||||
Desde comunicar un bug a solventar un error en el codigo, toda contribucion es apreciada y bienvenida. Si estas planeando
|
||||
implementar una nueva funcionalidad o un realizar un cambio en la API, por favor crea un [nuevo mensaje aqui](https://github.com/Budibase/budibase/issues),
|
||||
de esta manera nos encargaremos que tu trabajo no sea en vano.
|
||||
|
||||
Aqui tienes instrucciones de como configurar tu entorno Budibase para [Debian](https://github.com/Budibase/budibase/tree/HEAD/docs/DEV-SETUP-DEBIAN.md)
|
||||
y [MacOSX](https://github.com/Budibase/budibase/tree/HEAD/docs/DEV-SETUP-MACOSX.md)
|
||||
|
||||
### No estas seguro por donde empezar?
|
||||
Un buen lugar para empezar a contribuir con nosotros es [aqui](https://github.com/Budibase/budibase/projects/22).
|
||||
|
||||
### Organizacion del repositorio
|
||||
|
||||
Budibase es un repositorio unico gestionado por Lerna. Lerna construye y publica los paquetes de Budibase sincronizandolos
|
||||
cada ves que se realiza un cambio. A rasgos generales, estos son los paquetes que conforman Budibase:
|
||||
|
||||
- [packages/builder](https://github.com/Budibase/budibase/tree/HEAD/packages/builder) - contiene el codigo del builder de la parte cliente, esta es una aplicacion svelte.
|
||||
|
||||
- [packages/client](https://github.com/Budibase/budibase/tree/HEAD/packages/client) - Este modulo se ejecuta en el browser y es el responsable de leer definiciones JSON y crear aplicaciones web en el momento.
|
||||
|
||||
- [packages/server](https://github.com/Budibase/budibase/tree/HEAD/packages/server) - La parte servidor de Budibase. Esta aplicacion Koa es responsable de suministrar lo necesario al builder para asi generar las aplicaciones Budibase. Tambien provee una API para interaccionar con la base de datos y el almacenamiento de ficheros.
|
||||
|
||||
Para mas informacion, por favor lee el siguiente documento [CONTRIBUTING.md](https://github.com/Budibase/budibase/blob/HEAD/docs/CONTRIBUTING.md)
|
||||
|
||||
<br /><br />
|
||||
|
||||
|
||||
## 📝 Licencia
|
||||
|
||||
Budibase es open-source, licenciado como [GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html). El cliente y las librerias
|
||||
de componentes estan licenciadas como [MPL](https://directory.fsf.org/wiki/License:MPL-2.0) - de esta manera, puedes licenciar
|
||||
como tu quieras las aplicaciones que construyas.
|
||||
|
||||
<br /><br />
|
||||
|
||||
## ⭐ Historia de nuestros Stargazers
|
||||
|
||||
[![Stargazers over time](https://starchart.cc/Budibase/budibase.svg)](https://starchart.cc/Budibase/budibase)
|
||||
|
||||
If you are having issues between updates of the builder, please use the guide [here](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md#troubleshooting) to clear down your environment.
|
||||
Si estas teniendo problemas con el builder despues de actualizar, por favor [lee esta guia](https://github.com/Budibase/budibase/blob/HEAD/docs/CONTRIBUTING.md#troubleshooting) to clear down your environment.
|
||||
|
||||
<br /><br />
|
||||
|
||||
## 🏁 Getting Started with Budibase
|
||||
## Contribuidores ✨
|
||||
|
||||
The Budibase builder runs in Electron, on Mac, PC and Linux. Follow the steps below to get started:
|
||||
- [ ] [Sign-up to Budibase](https://portal.budi.live/signup)
|
||||
- [ ] Create a username and password
|
||||
- [ ] Copy your API key
|
||||
- [ ] Download Budibase
|
||||
- [ ] Open Budibase and enter your API key
|
||||
|
||||
[Here is a guided tutorial](https://docs.budibase.com/tutorial/tutorial-signing-up) if you need extra help.
|
||||
|
||||
|
||||
## 🤖 Self-hosting
|
||||
|
||||
Budibase wants to make sure anyone can use the tools we develop and we know a lot of people need to be able to host the apps they make on their own systems - that is why we've decided to try and make self hosting as easy as possible!
|
||||
|
||||
Currently, you can host your apps using Docker or Digital Ocean. The documentation for self-hosting can be found [here](https://docs.budibase.com/docs/hosting-methods).
|
||||
|
||||
[![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/droplets/new?onboarding_origin=marketplace&i=09038e&fleetUuid=bb04f9c8-1de8-4687-b2ae-1d5177a0535b&appId=77729671&type=applications&size=s-4vcpu-8gb®ion=nyc1&refcode=0caaa6085a82&image=budibase-20-04)
|
||||
|
||||
|
||||
## 🎓 Learning Budibase
|
||||
|
||||
The Budibase [documentation lives here](https://docs.budibase.com).
|
||||
|
||||
You can also follow a quick tutorial on [how to build a CRM with Budibase](https://docs.budibase.com/tutorial/tutorial-introduction)
|
||||
|
||||
|
||||
## Roadmap
|
||||
|
||||
Checkout our [Public Roadmap](https://github.com/Budibase/budibase/projects/10). If you would like to discuss some of the items on the roadmap, please feel to reach out on [Discord](https://discord.gg/rCYayfe), or via [Github discussions](https://github.com/Budibase/budibase/discussions)
|
||||
|
||||
|
||||
## ❗ Code of Conduct
|
||||
|
||||
Budibase is dedicated to providing a welcoming, diverse, and harrassment-free experience for everyone. We expect everyone in the Budibase community to abide by our [**Code of Conduct**](https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md). Please read it.
|
||||
|
||||
## 🙌 Contributing to Budibase
|
||||
|
||||
From opening a bug report to creating a pull request: every contribution is appreciated and welcomed. If you're planning to implement a new feature or change the API please create an issue first. This way we can ensure your work is not in vain.
|
||||
|
||||
### Not Sure Where to Start?
|
||||
A good place to start contributing, is the [First time issues project](https://github.com/Budibase/budibase/projects/22).
|
||||
|
||||
### How the repository is organized
|
||||
Budibase is a monorepo managed by lerna. Lerna manages the building and publishing of the budibase packages. At a high level, here are the packages that make up Budibase.
|
||||
|
||||
- [packages/builder](https://github.com/Budibase/budibase/tree/HEAD/packages/builder) - contains code for the budibase builder client side svelte application.
|
||||
|
||||
- [packages/client](https://github.com/Budibase/budibase/tree/HEAD/packages/client) - A module that runs in the browser responsible for reading JSON definition and creating living, breathing web apps from it.
|
||||
|
||||
- [packages/server](https://github.com/Budibase/budibase/tree/HEAD/packages/server) - The budibase server. This Koa app is responsible for serving the JS for the builder and budibase apps, as well as providing the API for interaction with the database and file system.
|
||||
|
||||
For more information, see [CONTRIBUTING.md](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md)
|
||||
|
||||
## 📝 License
|
||||
|
||||
Budibase is open-source. The builder is licensed [AGPL v3](https://www.gnu.org/licenses/agpl-3.0.en.html), the server is licensed [GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html), and the client is licensed [MPL](https://directory.fsf.org/wiki/License:MPL-2.0).
|
||||
|
||||
## 💬 Get in touch
|
||||
|
||||
If you have a question or would like to talk with other Budibase users, please hop over to [Github discussions](https://github.com/Budibase/budibase/discussions) or join our Discord server:
|
||||
|
||||
[Discord chatroom](https://discord.gg/rCYayfe)
|
||||
|
||||
![Discord Shield](https://discordapp.com/api/guilds/733030666647765003/widget.png?style=shield)
|
||||
|
||||
|
||||
## Contributors ✨
|
||||
|
||||
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||
Queremos prestar un especial agradecimiento a nuestra maravillosa gente ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
<!-- prettier-ignore-start -->
|
||||
|
@ -179,14 +260,18 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|||
<td align="center"><a href="https://budibase.com/"><img src="https://avatars3.githubusercontent.com/u/3524181?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Michael Shanks</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=mjashanks" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/kevmodrome"><img src="https://avatars3.githubusercontent.com/u/534488?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Kevin Åberg Kultalahti</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=kevmodrome" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://www.budibase.com/"><img src="https://avatars2.githubusercontent.com/u/49767913?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Joe</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=joebudi" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=joebudi" title="Code">💻</a> <a href="#content-joebudi" title="Content">🖋</a> <a href="#design-joebudi" title="Design">🎨</a></td>
|
||||
<td align="center"><a href="https://github.com/Conor-Mack"><img src="https://avatars1.githubusercontent.com/u/36074859?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Conor_Mack</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=Conor-Mack" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=Conor-Mack" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/Rory-Powell"><img src="https://avatars.githubusercontent.com/u/8755148?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Rory Powell</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Tests">⚠️</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/PClmnt"><img src="https://avatars.githubusercontent.com/u/5665926?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Peter Clement</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=PClmnt" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=PClmnt" title="Documentation">📖</a> <a href="https://github.com/Budibase/budibase/commits?author=PClmnt" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/Conor-Mack"><img src="https://avatars1.githubusercontent.com/u/36074859?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Conor_Mack</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=Conor-Mack" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=Conor-Mack" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/pngwn"><img src="https://avatars1.githubusercontent.com/u/12937446?v=4?s=100" width="100px;" alt=""/><br /><sub><b>pngwn</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=pngwn" title="Code">💻</a> <a href="https://github.com/Budibase/budibase/commits?author=pngwn" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/HugoLd"><img src="https://avatars0.githubusercontent.com/u/26521848?v=4?s=100" width="100px;" alt=""/><br /><sub><b>HugoLd</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=HugoLd" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/victoriasloan"><img src="https://avatars.githubusercontent.com/u/9913651?v=4?s=100" width="100px;" alt=""/><br /><sub><b>victoriasloan</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=victoriasloan" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/yashank09"><img src="https://avatars.githubusercontent.com/u/37672190?v=4?s=100" width="100px;" alt=""/><br /><sub><b>yashank09</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=yashank09" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/SOVLOOKUP"><img src="https://avatars.githubusercontent.com/u/53158137?v=4?s=100" width="100px;" alt=""/><br /><sub><b>SOVLOOKUP</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=SOVLOOKUP" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/seoulaja"><img src="https://avatars.githubusercontent.com/u/15101654?v=4?s=100" width="100px;" alt=""/><br /><sub><b>seoulaja</b></sub></a><br /><a href="#translation-seoulaja" title="Translation">🌍</a></td>
|
||||
<td align="center"><a href="https://github.com/mslourens"><img src="https://avatars.githubusercontent.com/u/1907152?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Maurits Lourens</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=mslourens" title="Tests">⚠️</a> <a href="https://github.com/Budibase/budibase/commits?author=mslourens" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
@ -195,4 +280,5 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
||||
Este proyecto sigue las especificaciones de [all-contributors](https://github.com/all-contributors/all-contributors).
|
||||
Todo tipo de contribuciones son agradecidas!
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "1.1.29-alpha.2",
|
||||
"version": "1.2.39-alpha.5",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
"build": "lerna run build",
|
||||
"build:dev": "lerna run prebuild && tsc --build --watch --preserveWatchOutput",
|
||||
"release": "lerna publish ${RELEASE_VERSION_TYPE:-patch} --yes --force-publish && yarn release:pro",
|
||||
"release:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop && yarn release:pro:develop",
|
||||
"release:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop --exact && yarn release:pro:develop",
|
||||
"release:pro": "bash scripts/pro/release.sh",
|
||||
"release:pro:develop": "bash scripts/pro/release.sh develop",
|
||||
"restore": "yarn run clean && yarn run bootstrap && yarn run build",
|
||||
|
@ -85,4 +85,4 @@
|
|||
"install:pro": "bash scripts/pro/install.sh",
|
||||
"dep:clean": "yarn clean && yarn bootstrap"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/backend-core",
|
||||
"version": "1.1.29-alpha.2",
|
||||
"version": "1.2.39-alpha.5",
|
||||
"description": "Budibase backend core libraries used in server and worker",
|
||||
"main": "dist/src/index.js",
|
||||
"types": "dist/src/index.d.ts",
|
||||
|
@ -20,13 +20,14 @@
|
|||
"test:watch": "jest --watchAll"
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/types": "^1.1.29-alpha.2",
|
||||
"@budibase/types": "1.2.39-alpha.5",
|
||||
"@techpass/passport-openidconnect": "0.3.2",
|
||||
"aws-sdk": "2.1030.0",
|
||||
"bcrypt": "5.0.1",
|
||||
"dotenv": "16.0.1",
|
||||
"emitter-listener": "1.1.2",
|
||||
"ioredis": "4.28.0",
|
||||
"joi": "17.6.0",
|
||||
"jsonwebtoken": "8.5.1",
|
||||
"koa-passport": "4.1.4",
|
||||
"lodash": "4.17.21",
|
||||
|
|
|
@ -19,6 +19,8 @@ const {
|
|||
csrf,
|
||||
internalApi,
|
||||
adminOnly,
|
||||
builderOnly,
|
||||
builderOrAdmin,
|
||||
joiValidator,
|
||||
} = require("./middleware")
|
||||
|
||||
|
@ -176,5 +178,7 @@ module.exports = {
|
|||
updateUserOAuth,
|
||||
ssoCallbackUrl,
|
||||
adminOnly,
|
||||
builderOnly,
|
||||
builderOrAdmin,
|
||||
joiValidator,
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const redis = require("../redis/init")
|
||||
const { doWithDB } = require("../db")
|
||||
const { DocumentTypes } = require("../db/constants")
|
||||
const { DocumentType } = require("../db/constants")
|
||||
|
||||
const AppState = {
|
||||
INVALID: "invalid",
|
||||
|
@ -14,7 +14,7 @@ const populateFromDB = async appId => {
|
|||
return doWithDB(
|
||||
appId,
|
||||
db => {
|
||||
return db.get(DocumentTypes.APP_METADATA)
|
||||
return db.get(DocumentType.APP_METADATA)
|
||||
},
|
||||
{ skip_setup: true }
|
||||
)
|
||||
|
|
|
@ -9,6 +9,7 @@ exports.CacheKeys = {
|
|||
UNIQUE_TENANT_ID: "uniqueTenantId",
|
||||
EVENTS: "events",
|
||||
BACKFILL_METADATA: "backfillMetadata",
|
||||
EVENTS_RATE_LIMIT: "eventsRateLimit",
|
||||
}
|
||||
|
||||
exports.TTL = {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export enum ContextKeys {
|
||||
export enum ContextKey {
|
||||
TENANT_ID = "tenantId",
|
||||
GLOBAL_DB = "globalDb",
|
||||
APP_ID = "appId",
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import env from "../environment"
|
||||
import { SEPARATOR, DocumentTypes } from "../db/constants"
|
||||
import { SEPARATOR, DocumentType } from "../db/constants"
|
||||
import cls from "./FunctionContext"
|
||||
import { dangerousGetDB, closeDB } from "../db"
|
||||
import { baseGlobalDBName } from "../tenancy/utils"
|
||||
import { IdentityContext } from "@budibase/types"
|
||||
import { DEFAULT_TENANT_ID as _DEFAULT_TENANT_ID } from "../constants"
|
||||
import { ContextKeys } from "./constants"
|
||||
import { ContextKey } from "./constants"
|
||||
import {
|
||||
updateUsing,
|
||||
closeWithUsing,
|
||||
|
@ -33,8 +33,8 @@ export const closeTenancy = async () => {
|
|||
}
|
||||
await closeDB(db)
|
||||
// clear from context now that database is closed/task is finished
|
||||
cls.setOnContext(ContextKeys.TENANT_ID, null)
|
||||
cls.setOnContext(ContextKeys.GLOBAL_DB, null)
|
||||
cls.setOnContext(ContextKey.TENANT_ID, null)
|
||||
cls.setOnContext(ContextKey.GLOBAL_DB, null)
|
||||
}
|
||||
|
||||
// export const isDefaultTenant = () => {
|
||||
|
@ -54,7 +54,7 @@ export const getTenantIDFromAppID = (appId: string) => {
|
|||
return null
|
||||
}
|
||||
const split = appId.split(SEPARATOR)
|
||||
const hasDev = split[1] === DocumentTypes.DEV
|
||||
const hasDev = split[1] === DocumentType.DEV
|
||||
if ((hasDev && split.length === 3) || (!hasDev && split.length === 2)) {
|
||||
return null
|
||||
}
|
||||
|
@ -83,14 +83,14 @@ export const doInTenant = (tenantId: string | null, task: any) => {
|
|||
// invoke the task
|
||||
return await task()
|
||||
} finally {
|
||||
await closeWithUsing(ContextKeys.TENANCY_IN_USE, () => {
|
||||
await closeWithUsing(ContextKey.TENANCY_IN_USE, () => {
|
||||
return closeTenancy()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const existing = cls.getFromContext(ContextKeys.TENANT_ID) === tenantId
|
||||
return updateUsing(ContextKeys.TENANCY_IN_USE, existing, internal)
|
||||
const existing = cls.getFromContext(ContextKey.TENANT_ID) === tenantId
|
||||
return updateUsing(ContextKey.TENANCY_IN_USE, existing, internal)
|
||||
}
|
||||
|
||||
export const doInAppContext = (appId: string, task: any) => {
|
||||
|
@ -108,7 +108,7 @@ export const doInAppContext = (appId: string, task: any) => {
|
|||
setAppTenantId(appId)
|
||||
}
|
||||
// set the app ID
|
||||
cls.setOnContext(ContextKeys.APP_ID, appId)
|
||||
cls.setOnContext(ContextKey.APP_ID, appId)
|
||||
|
||||
// preserve the identity
|
||||
if (identity) {
|
||||
|
@ -118,14 +118,14 @@ export const doInAppContext = (appId: string, task: any) => {
|
|||
// invoke the task
|
||||
return await task()
|
||||
} finally {
|
||||
await closeWithUsing(ContextKeys.APP_IN_USE, async () => {
|
||||
await closeWithUsing(ContextKey.APP_IN_USE, async () => {
|
||||
await closeAppDBs()
|
||||
await closeTenancy()
|
||||
})
|
||||
}
|
||||
}
|
||||
const existing = cls.getFromContext(ContextKeys.APP_ID) === appId
|
||||
return updateUsing(ContextKeys.APP_IN_USE, existing, internal)
|
||||
const existing = cls.getFromContext(ContextKey.APP_ID) === appId
|
||||
return updateUsing(ContextKey.APP_IN_USE, existing, internal)
|
||||
}
|
||||
|
||||
export const doInIdentityContext = (identity: IdentityContext, task: any) => {
|
||||
|
@ -135,7 +135,7 @@ export const doInIdentityContext = (identity: IdentityContext, task: any) => {
|
|||
|
||||
async function internal(opts = { existing: false }) {
|
||||
if (!opts.existing) {
|
||||
cls.setOnContext(ContextKeys.IDENTITY, identity)
|
||||
cls.setOnContext(ContextKey.IDENTITY, identity)
|
||||
// set the tenant so that doInTenant will preserve identity
|
||||
if (identity.tenantId) {
|
||||
updateTenantId(identity.tenantId)
|
||||
|
@ -146,27 +146,27 @@ export const doInIdentityContext = (identity: IdentityContext, task: any) => {
|
|||
// invoke the task
|
||||
return await task()
|
||||
} finally {
|
||||
await closeWithUsing(ContextKeys.IDENTITY_IN_USE, async () => {
|
||||
await closeWithUsing(ContextKey.IDENTITY_IN_USE, async () => {
|
||||
setIdentity(null)
|
||||
await closeTenancy()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const existing = cls.getFromContext(ContextKeys.IDENTITY)
|
||||
return updateUsing(ContextKeys.IDENTITY_IN_USE, existing, internal)
|
||||
const existing = cls.getFromContext(ContextKey.IDENTITY)
|
||||
return updateUsing(ContextKey.IDENTITY_IN_USE, existing, internal)
|
||||
}
|
||||
|
||||
export const getIdentity = (): IdentityContext | undefined => {
|
||||
try {
|
||||
return cls.getFromContext(ContextKeys.IDENTITY)
|
||||
return cls.getFromContext(ContextKey.IDENTITY)
|
||||
} catch (e) {
|
||||
// do nothing - identity is not in context
|
||||
}
|
||||
}
|
||||
|
||||
export const updateTenantId = (tenantId: string | null) => {
|
||||
cls.setOnContext(ContextKeys.TENANT_ID, tenantId)
|
||||
cls.setOnContext(ContextKey.TENANT_ID, tenantId)
|
||||
if (env.USE_COUCH) {
|
||||
setGlobalDB(tenantId)
|
||||
}
|
||||
|
@ -176,7 +176,7 @@ export const updateAppId = async (appId: string) => {
|
|||
try {
|
||||
// have to close first, before removing the databases from context
|
||||
await closeAppDBs()
|
||||
cls.setOnContext(ContextKeys.APP_ID, appId)
|
||||
cls.setOnContext(ContextKey.APP_ID, appId)
|
||||
} catch (err) {
|
||||
if (env.isTest()) {
|
||||
TEST_APP_ID = appId
|
||||
|
@ -189,12 +189,12 @@ export const updateAppId = async (appId: string) => {
|
|||
export const setGlobalDB = (tenantId: string | null) => {
|
||||
const dbName = baseGlobalDBName(tenantId)
|
||||
const db = dangerousGetDB(dbName)
|
||||
cls.setOnContext(ContextKeys.GLOBAL_DB, db)
|
||||
cls.setOnContext(ContextKey.GLOBAL_DB, db)
|
||||
return db
|
||||
}
|
||||
|
||||
export const getGlobalDB = () => {
|
||||
const db = cls.getFromContext(ContextKeys.GLOBAL_DB)
|
||||
const db = cls.getFromContext(ContextKey.GLOBAL_DB)
|
||||
if (!db) {
|
||||
throw new Error("Global DB not found")
|
||||
}
|
||||
|
@ -202,7 +202,7 @@ export const getGlobalDB = () => {
|
|||
}
|
||||
|
||||
export const isTenantIdSet = () => {
|
||||
const tenantId = cls.getFromContext(ContextKeys.TENANT_ID)
|
||||
const tenantId = cls.getFromContext(ContextKey.TENANT_ID)
|
||||
return !!tenantId
|
||||
}
|
||||
|
||||
|
@ -210,7 +210,7 @@ export const getTenantId = () => {
|
|||
if (!isMultiTenant()) {
|
||||
return DEFAULT_TENANT_ID
|
||||
}
|
||||
const tenantId = cls.getFromContext(ContextKeys.TENANT_ID)
|
||||
const tenantId = cls.getFromContext(ContextKey.TENANT_ID)
|
||||
if (!tenantId) {
|
||||
throw new Error("Tenant id not found")
|
||||
}
|
||||
|
@ -218,7 +218,7 @@ export const getTenantId = () => {
|
|||
}
|
||||
|
||||
export const getAppId = () => {
|
||||
const foundId = cls.getFromContext(ContextKeys.APP_ID)
|
||||
const foundId = cls.getFromContext(ContextKey.APP_ID)
|
||||
if (!foundId && env.isTest() && TEST_APP_ID) {
|
||||
return TEST_APP_ID
|
||||
} else {
|
||||
|
@ -231,7 +231,7 @@ export const getAppId = () => {
|
|||
* contained, dev or prod.
|
||||
*/
|
||||
export const getAppDB = (opts?: any) => {
|
||||
return getContextDB(ContextKeys.CURRENT_DB, opts)
|
||||
return getContextDB(ContextKey.CURRENT_DB, opts)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -239,7 +239,7 @@ export const getAppDB = (opts?: any) => {
|
|||
* contained a development app ID, this will open the prod one.
|
||||
*/
|
||||
export const getProdAppDB = (opts?: any) => {
|
||||
return getContextDB(ContextKeys.PROD_DB, opts)
|
||||
return getContextDB(ContextKey.PROD_DB, opts)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -247,5 +247,5 @@ export const getProdAppDB = (opts?: any) => {
|
|||
* contained a prod app ID, this will open the dev one.
|
||||
*/
|
||||
export const getDevAppDB = (opts?: any) => {
|
||||
return getContextDB(ContextKeys.DEV_DB, opts)
|
||||
return getContextDB(ContextKey.DEV_DB, opts)
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
} from "./index"
|
||||
import cls from "./FunctionContext"
|
||||
import { IdentityContext } from "@budibase/types"
|
||||
import { ContextKeys } from "./constants"
|
||||
import { ContextKey } from "./constants"
|
||||
import { dangerousGetDB, closeDB } from "../db"
|
||||
import { isEqual } from "lodash"
|
||||
import { getDevelopmentAppID, getProdAppID } from "../db/conversions"
|
||||
|
@ -47,17 +47,13 @@ export const setAppTenantId = (appId: string) => {
|
|||
}
|
||||
|
||||
export const setIdentity = (identity: IdentityContext | null) => {
|
||||
cls.setOnContext(ContextKeys.IDENTITY, identity)
|
||||
cls.setOnContext(ContextKey.IDENTITY, identity)
|
||||
}
|
||||
|
||||
// this function makes sure the PouchDB objects are closed and
|
||||
// fully deleted when finished - this protects against memory leaks
|
||||
export async function closeAppDBs() {
|
||||
const dbKeys = [
|
||||
ContextKeys.CURRENT_DB,
|
||||
ContextKeys.PROD_DB,
|
||||
ContextKeys.DEV_DB,
|
||||
]
|
||||
const dbKeys = [ContextKey.CURRENT_DB, ContextKey.PROD_DB, ContextKey.DEV_DB]
|
||||
for (let dbKey of dbKeys) {
|
||||
const db = cls.getFromContext(dbKey)
|
||||
if (!db) {
|
||||
|
@ -68,16 +64,16 @@ export async function closeAppDBs() {
|
|||
cls.setOnContext(dbKey, null)
|
||||
}
|
||||
// clear the app ID now that the databases are closed
|
||||
if (cls.getFromContext(ContextKeys.APP_ID)) {
|
||||
cls.setOnContext(ContextKeys.APP_ID, null)
|
||||
if (cls.getFromContext(ContextKey.APP_ID)) {
|
||||
cls.setOnContext(ContextKey.APP_ID, null)
|
||||
}
|
||||
if (cls.getFromContext(ContextKeys.DB_OPTS)) {
|
||||
cls.setOnContext(ContextKeys.DB_OPTS, null)
|
||||
if (cls.getFromContext(ContextKey.DB_OPTS)) {
|
||||
cls.setOnContext(ContextKey.DB_OPTS, null)
|
||||
}
|
||||
}
|
||||
|
||||
export function getContextDB(key: string, opts: any) {
|
||||
const dbOptsKey = `${key}${ContextKeys.DB_OPTS}`
|
||||
const dbOptsKey = `${key}${ContextKey.DB_OPTS}`
|
||||
let storedOpts = cls.getFromContext(dbOptsKey)
|
||||
let db = cls.getFromContext(key)
|
||||
if (db && isEqual(opts, storedOpts)) {
|
||||
|
@ -88,13 +84,13 @@ export function getContextDB(key: string, opts: any) {
|
|||
let toUseAppId
|
||||
|
||||
switch (key) {
|
||||
case ContextKeys.CURRENT_DB:
|
||||
case ContextKey.CURRENT_DB:
|
||||
toUseAppId = appId
|
||||
break
|
||||
case ContextKeys.PROD_DB:
|
||||
case ContextKey.PROD_DB:
|
||||
toUseAppId = getProdAppID(appId)
|
||||
break
|
||||
case ContextKeys.DEV_DB:
|
||||
case ContextKey.DEV_DB:
|
||||
toUseAppId = getDevelopmentAppID(appId)
|
||||
break
|
||||
}
|
||||
|
|
|
@ -4,13 +4,13 @@ export const UNICODE_MAX = "\ufff0"
|
|||
/**
|
||||
* Can be used to create a few different forms of querying a view.
|
||||
*/
|
||||
export enum AutomationViewModes {
|
||||
export enum AutomationViewMode {
|
||||
ALL = "all",
|
||||
AUTOMATION = "automation",
|
||||
STATUS = "status",
|
||||
}
|
||||
|
||||
export enum ViewNames {
|
||||
export enum ViewName {
|
||||
USER_BY_APP = "by_app",
|
||||
USER_BY_EMAIL = "by_email2",
|
||||
BY_API_KEY = "by_api_key",
|
||||
|
@ -21,13 +21,13 @@ export enum ViewNames {
|
|||
}
|
||||
|
||||
export const DeprecatedViews = {
|
||||
[ViewNames.USER_BY_EMAIL]: [
|
||||
[ViewName.USER_BY_EMAIL]: [
|
||||
// removed due to inaccuracy in view doc filter logic
|
||||
"by_email",
|
||||
],
|
||||
}
|
||||
|
||||
export enum DocumentTypes {
|
||||
export enum DocumentType {
|
||||
USER = "us",
|
||||
GROUP = "gr",
|
||||
WORKSPACE = "workspace",
|
||||
|
@ -62,6 +62,6 @@ export const StaticDatabases = {
|
|||
},
|
||||
}
|
||||
|
||||
export const APP_PREFIX = exports.DocumentTypes.APP + exports.SEPARATOR
|
||||
export const APP_DEV = exports.DocumentTypes.APP_DEV + exports.SEPARATOR
|
||||
export const APP_PREFIX = DocumentType.APP + SEPARATOR
|
||||
export const APP_DEV = DocumentType.APP_DEV + SEPARATOR
|
||||
export const APP_DEV_PREFIX = APP_DEV
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { newid } from "../hashing"
|
||||
import { DEFAULT_TENANT_ID, Configs } from "../constants"
|
||||
import env from "../environment"
|
||||
import { SEPARATOR, DocumentTypes, UNICODE_MAX, ViewNames } from "./constants"
|
||||
import { SEPARATOR, DocumentType, UNICODE_MAX, ViewName } from "./constants"
|
||||
import { getTenantId, getGlobalDBName, getGlobalDB } from "../tenancy"
|
||||
import fetch from "node-fetch"
|
||||
import { doWithDB, allDbs } from "./index"
|
||||
|
@ -58,7 +58,7 @@ export function getDocParams(
|
|||
/**
|
||||
* Retrieve the correct index for a view based on default design DB.
|
||||
*/
|
||||
export function getQueryIndex(viewName: ViewNames) {
|
||||
export function getQueryIndex(viewName: ViewName) {
|
||||
return `database/${viewName}`
|
||||
}
|
||||
|
||||
|
@ -67,7 +67,7 @@ export function getQueryIndex(viewName: ViewNames) {
|
|||
* @returns {string} The new workspace ID which the workspace doc can be stored under.
|
||||
*/
|
||||
export function generateWorkspaceID() {
|
||||
return `${DocumentTypes.WORKSPACE}${SEPARATOR}${newid()}`
|
||||
return `${DocumentType.WORKSPACE}${SEPARATOR}${newid()}`
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -76,8 +76,8 @@ export function generateWorkspaceID() {
|
|||
export function getWorkspaceParams(id = "", otherProps = {}) {
|
||||
return {
|
||||
...otherProps,
|
||||
startkey: `${DocumentTypes.WORKSPACE}${SEPARATOR}${id}`,
|
||||
endkey: `${DocumentTypes.WORKSPACE}${SEPARATOR}${id}${UNICODE_MAX}`,
|
||||
startkey: `${DocumentType.WORKSPACE}${SEPARATOR}${id}`,
|
||||
endkey: `${DocumentType.WORKSPACE}${SEPARATOR}${id}${UNICODE_MAX}`,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -86,7 +86,7 @@ export function getWorkspaceParams(id = "", otherProps = {}) {
|
|||
* @returns {string} The new user ID which the user doc can be stored under.
|
||||
*/
|
||||
export function generateGlobalUserID(id?: any) {
|
||||
return `${DocumentTypes.USER}${SEPARATOR}${id || newid()}`
|
||||
return `${DocumentType.USER}${SEPARATOR}${id || newid()}`
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -102,8 +102,8 @@ export function getGlobalUserParams(globalId: any, otherProps: any = {}) {
|
|||
// need to include this incase pagination
|
||||
startkey: startkey
|
||||
? startkey
|
||||
: `${DocumentTypes.USER}${SEPARATOR}${globalId}`,
|
||||
endkey: `${DocumentTypes.USER}${SEPARATOR}${globalId}${UNICODE_MAX}`,
|
||||
: `${DocumentType.USER}${SEPARATOR}${globalId}`,
|
||||
endkey: `${DocumentType.USER}${SEPARATOR}${globalId}${UNICODE_MAX}`,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -121,7 +121,7 @@ export function getUsersByAppParams(appId: any, otherProps: any = {}) {
|
|||
* @param ownerId The owner/user of the template, this could be global or a workspace level.
|
||||
*/
|
||||
export function generateTemplateID(ownerId: any) {
|
||||
return `${DocumentTypes.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}${newid()}`
|
||||
return `${DocumentType.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}${newid()}`
|
||||
}
|
||||
|
||||
export function generateAppUserID(prodAppId: string, userId: string) {
|
||||
|
@ -143,7 +143,7 @@ export function getTemplateParams(
|
|||
if (templateId) {
|
||||
final = templateId
|
||||
} else {
|
||||
final = `${DocumentTypes.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}`
|
||||
final = `${DocumentType.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}`
|
||||
}
|
||||
return {
|
||||
...otherProps,
|
||||
|
@ -157,14 +157,14 @@ export function getTemplateParams(
|
|||
* @returns {string} The new role ID which the role doc can be stored under.
|
||||
*/
|
||||
export function generateRoleID(id: any) {
|
||||
return `${DocumentTypes.ROLE}${SEPARATOR}${id || newid()}`
|
||||
return `${DocumentType.ROLE}${SEPARATOR}${id || newid()}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets parameters for retrieving a role, this is a utility function for the getDocParams function.
|
||||
*/
|
||||
export function getRoleParams(roleId = null, otherProps = {}) {
|
||||
return getDocParams(DocumentTypes.ROLE, roleId, otherProps)
|
||||
return getDocParams(DocumentType.ROLE, roleId, otherProps)
|
||||
}
|
||||
|
||||
export function getStartEndKeyURL(base: any, baseKey: any, tenantId = null) {
|
||||
|
@ -211,9 +211,9 @@ export async function getAllDbs(opts = { efficient: false }) {
|
|||
await addDbs(couchUrl)
|
||||
} else {
|
||||
// get prod apps
|
||||
await addDbs(getStartEndKeyURL(couchUrl, DocumentTypes.APP, tenantId))
|
||||
await addDbs(getStartEndKeyURL(couchUrl, DocumentType.APP, tenantId))
|
||||
// get dev apps
|
||||
await addDbs(getStartEndKeyURL(couchUrl, DocumentTypes.APP_DEV, tenantId))
|
||||
await addDbs(getStartEndKeyURL(couchUrl, DocumentType.APP_DEV, tenantId))
|
||||
// add global db name
|
||||
dbs.push(getGlobalDBName(tenantId))
|
||||
}
|
||||
|
@ -233,14 +233,18 @@ export async function getAllApps({ dev, all, idsOnly, efficient }: any = {}) {
|
|||
}
|
||||
let dbs = await getAllDbs({ efficient })
|
||||
const appDbNames = dbs.filter((dbName: any) => {
|
||||
if (env.isTest() && !dbName) {
|
||||
return false
|
||||
}
|
||||
|
||||
const split = dbName.split(SEPARATOR)
|
||||
// it is an app, check the tenantId
|
||||
if (split[0] === DocumentTypes.APP) {
|
||||
if (split[0] === DocumentType.APP) {
|
||||
// tenantId is always right before the UUID
|
||||
const possibleTenantId = split[split.length - 2]
|
||||
|
||||
const noTenantId =
|
||||
split.length === 2 || possibleTenantId === DocumentTypes.DEV
|
||||
split.length === 2 || possibleTenantId === DocumentType.DEV
|
||||
|
||||
return (
|
||||
(tenantId === DEFAULT_TENANT_ID && noTenantId) ||
|
||||
|
@ -326,7 +330,7 @@ export async function dbExists(dbName: any) {
|
|||
export const generateConfigID = ({ type, workspace, user }: any) => {
|
||||
const scope = [type, workspace, user].filter(Boolean).join(SEPARATOR)
|
||||
|
||||
return `${DocumentTypes.CONFIG}${SEPARATOR}${scope}`
|
||||
return `${DocumentType.CONFIG}${SEPARATOR}${scope}`
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -340,8 +344,8 @@ export const getConfigParams = (
|
|||
|
||||
return {
|
||||
...otherProps,
|
||||
startkey: `${DocumentTypes.CONFIG}${SEPARATOR}${scope}`,
|
||||
endkey: `${DocumentTypes.CONFIG}${SEPARATOR}${scope}${UNICODE_MAX}`,
|
||||
startkey: `${DocumentType.CONFIG}${SEPARATOR}${scope}`,
|
||||
endkey: `${DocumentType.CONFIG}${SEPARATOR}${scope}${UNICODE_MAX}`,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -350,7 +354,7 @@ export const getConfigParams = (
|
|||
* @returns {string} The new dev info ID which info for dev (like api key) can be stored under.
|
||||
*/
|
||||
export const generateDevInfoID = (userId: any) => {
|
||||
return `${DocumentTypes.DEV_INFO}${SEPARATOR}${userId}`
|
||||
return `${DocumentType.DEV_INFO}${SEPARATOR}${userId}`
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const {
|
||||
DocumentTypes,
|
||||
ViewNames,
|
||||
DocumentType,
|
||||
ViewName,
|
||||
DeprecatedViews,
|
||||
SEPARATOR,
|
||||
} = require("./utils")
|
||||
|
@ -44,14 +44,14 @@ exports.createNewUserEmailView = async () => {
|
|||
const view = {
|
||||
// if using variables in a map function need to inject them before use
|
||||
map: `function(doc) {
|
||||
if (doc._id.startsWith("${DocumentTypes.USER}${SEPARATOR}")) {
|
||||
if (doc._id.startsWith("${DocumentType.USER}${SEPARATOR}")) {
|
||||
emit(doc.email.toLowerCase(), doc._id)
|
||||
}
|
||||
}`,
|
||||
}
|
||||
designDoc.views = {
|
||||
...designDoc.views,
|
||||
[ViewNames.USER_BY_EMAIL]: view,
|
||||
[ViewName.USER_BY_EMAIL]: view,
|
||||
}
|
||||
await db.put(designDoc)
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ exports.createUserAppView = async () => {
|
|||
const view = {
|
||||
// if using variables in a map function need to inject them before use
|
||||
map: `function(doc) {
|
||||
if (doc._id.startsWith("${DocumentTypes.USER}${SEPARATOR}") && doc.roles) {
|
||||
if (doc._id.startsWith("${DocumentType.USER}${SEPARATOR}") && doc.roles) {
|
||||
for (let prodAppId of Object.keys(doc.roles)) {
|
||||
let emitted = prodAppId + "${SEPARATOR}" + doc._id
|
||||
emit(emitted, null)
|
||||
|
@ -78,7 +78,7 @@ exports.createUserAppView = async () => {
|
|||
}
|
||||
designDoc.views = {
|
||||
...designDoc.views,
|
||||
[ViewNames.USER_BY_APP]: view,
|
||||
[ViewName.USER_BY_APP]: view,
|
||||
}
|
||||
await db.put(designDoc)
|
||||
}
|
||||
|
@ -93,14 +93,14 @@ exports.createApiKeyView = async () => {
|
|||
}
|
||||
const view = {
|
||||
map: `function(doc) {
|
||||
if (doc._id.startsWith("${DocumentTypes.DEV_INFO}") && doc.apiKey) {
|
||||
if (doc._id.startsWith("${DocumentType.DEV_INFO}") && doc.apiKey) {
|
||||
emit(doc.apiKey, doc.userId)
|
||||
}
|
||||
}`,
|
||||
}
|
||||
designDoc.views = {
|
||||
...designDoc.views,
|
||||
[ViewNames.BY_API_KEY]: view,
|
||||
[ViewName.BY_API_KEY]: view,
|
||||
}
|
||||
await db.put(designDoc)
|
||||
}
|
||||
|
@ -123,17 +123,17 @@ exports.createUserBuildersView = async () => {
|
|||
}
|
||||
designDoc.views = {
|
||||
...designDoc.views,
|
||||
[ViewNames.USER_BY_BUILDERS]: view,
|
||||
[ViewName.USER_BY_BUILDERS]: view,
|
||||
}
|
||||
await db.put(designDoc)
|
||||
}
|
||||
|
||||
exports.queryGlobalView = async (viewName, params, db = null) => {
|
||||
const CreateFuncByName = {
|
||||
[ViewNames.USER_BY_EMAIL]: exports.createNewUserEmailView,
|
||||
[ViewNames.BY_API_KEY]: exports.createApiKeyView,
|
||||
[ViewNames.USER_BY_BUILDERS]: exports.createUserBuildersView,
|
||||
[ViewNames.USER_BY_APP]: exports.createUserAppView,
|
||||
[ViewName.USER_BY_EMAIL]: exports.createNewUserEmailView,
|
||||
[ViewName.BY_API_KEY]: exports.createApiKeyView,
|
||||
[ViewName.USER_BY_BUILDERS]: exports.createUserBuildersView,
|
||||
[ViewName.USER_BY_APP]: exports.createUserAppView,
|
||||
}
|
||||
// can pass DB in if working with something specific
|
||||
if (!db) {
|
||||
|
|
|
@ -55,6 +55,8 @@ const env = {
|
|||
DEFAULT_LICENSE: process.env.DEFAULT_LICENSE,
|
||||
SERVICE: process.env.SERVICE || "budibase",
|
||||
MEMORY_LEAK_CHECK: process.env.MEMORY_LEAK_CHECK || false,
|
||||
LOG_LEVEL: process.env.LOG_LEVEL,
|
||||
SESSION_UPDATE_PERIOD: process.env.SESSION_UPDATE_PERIOD,
|
||||
DEPLOYMENT_ENVIRONMENT:
|
||||
process.env.DEPLOYMENT_ENVIRONMENT || "docker-compose",
|
||||
_set(key: any, value: any) {
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Event, Identity, Group, IdentityType } from "@budibase/types"
|
|||
import { EventProcessor } from "./types"
|
||||
import env from "../../environment"
|
||||
import * as analytics from "../analytics"
|
||||
import PosthogProcessor from "./PosthogProcessor"
|
||||
import PosthogProcessor from "./posthog"
|
||||
|
||||
/**
|
||||
* Events that are always captured.
|
||||
|
@ -32,7 +32,7 @@ export default class AnalyticsProcessor implements EventProcessor {
|
|||
return
|
||||
}
|
||||
if (this.posthog) {
|
||||
this.posthog.processEvent(event, identity, properties, timestamp)
|
||||
await this.posthog.processEvent(event, identity, properties, timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -45,14 +45,14 @@ export default class AnalyticsProcessor implements EventProcessor {
|
|||
return
|
||||
}
|
||||
if (this.posthog) {
|
||||
this.posthog.identify(identity, timestamp)
|
||||
await this.posthog.identify(identity, timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
async identifyGroup(group: Group, timestamp?: string | number) {
|
||||
// Group indentifications (tenant and installation) always on
|
||||
if (this.posthog) {
|
||||
this.posthog.identifyGroup(group, timestamp)
|
||||
await this.posthog.identifyGroup(group, timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,26 @@
|
|||
import PostHog from "posthog-node"
|
||||
import { Event, Identity, Group, BaseEvent } from "@budibase/types"
|
||||
import { EventProcessor } from "./types"
|
||||
import env from "../../environment"
|
||||
import * as context from "../../context"
|
||||
const pkg = require("../../../package.json")
|
||||
import { EventProcessor } from "../types"
|
||||
import env from "../../../environment"
|
||||
import * as context from "../../../context"
|
||||
import * as rateLimiting from "./rateLimiting"
|
||||
const pkg = require("../../../../package.json")
|
||||
|
||||
const EXCLUDED_EVENTS: Event[] = [
|
||||
Event.USER_UPDATED,
|
||||
Event.EMAIL_SMTP_UPDATED,
|
||||
Event.AUTH_SSO_UPDATED,
|
||||
Event.APP_UPDATED,
|
||||
Event.ROLE_UPDATED,
|
||||
Event.DATASOURCE_UPDATED,
|
||||
Event.QUERY_UPDATED,
|
||||
Event.TABLE_UPDATED,
|
||||
Event.VIEW_UPDATED,
|
||||
Event.VIEW_FILTER_UPDATED,
|
||||
Event.VIEW_CALCULATION_UPDATED,
|
||||
Event.AUTOMATION_TRIGGER_UPDATED,
|
||||
Event.USER_GROUP_UPDATED,
|
||||
]
|
||||
|
||||
export default class PosthogProcessor implements EventProcessor {
|
||||
posthog: PostHog
|
||||
|
@ -21,6 +38,15 @@ export default class PosthogProcessor implements EventProcessor {
|
|||
properties: BaseEvent,
|
||||
timestamp?: string | number
|
||||
): Promise<void> {
|
||||
// don't send excluded events
|
||||
if (EXCLUDED_EVENTS.includes(event)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (await rateLimiting.limited(event)) {
|
||||
return
|
||||
}
|
||||
|
||||
properties.version = pkg.version
|
||||
properties.service = env.SERVICE
|
||||
properties.environment = identity.environment
|
|
@ -0,0 +1,2 @@
|
|||
import PosthogProcessor from "./PosthogProcessor"
|
||||
export default PosthogProcessor
|
|
@ -0,0 +1,106 @@
|
|||
import { Event } from "@budibase/types"
|
||||
import { CacheKeys, TTL } from "../../../cache/generic"
|
||||
import * as cache from "../../../cache/generic"
|
||||
import * as context from "../../../context"
|
||||
|
||||
type RateLimitedEvent =
|
||||
| Event.SERVED_BUILDER
|
||||
| Event.SERVED_APP_PREVIEW
|
||||
| Event.SERVED_APP
|
||||
|
||||
const isRateLimited = (event: Event): event is RateLimitedEvent => {
|
||||
return (
|
||||
event === Event.SERVED_BUILDER ||
|
||||
event === Event.SERVED_APP_PREVIEW ||
|
||||
event === Event.SERVED_APP
|
||||
)
|
||||
}
|
||||
|
||||
const isPerApp = (event: RateLimitedEvent) => {
|
||||
return event === Event.SERVED_APP_PREVIEW || event === Event.SERVED_APP
|
||||
}
|
||||
|
||||
interface EventProperties {
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
enum RateLimit {
|
||||
CALENDAR_DAY = "calendarDay",
|
||||
}
|
||||
|
||||
const RATE_LIMITS = {
|
||||
[Event.SERVED_APP]: RateLimit.CALENDAR_DAY,
|
||||
[Event.SERVED_APP_PREVIEW]: RateLimit.CALENDAR_DAY,
|
||||
[Event.SERVED_BUILDER]: RateLimit.CALENDAR_DAY,
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this event should be sent right now
|
||||
* Return false to signal the event SHOULD be sent
|
||||
* Return true to signal the event should NOT be sent
|
||||
*/
|
||||
export const limited = async (event: Event): Promise<boolean> => {
|
||||
// not a rate limited event -- send
|
||||
if (!isRateLimited(event)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const cachedEvent = await readEvent(event)
|
||||
if (cachedEvent) {
|
||||
const timestamp = new Date(cachedEvent.timestamp)
|
||||
const limit = RATE_LIMITS[event]
|
||||
switch (limit) {
|
||||
case RateLimit.CALENDAR_DAY: {
|
||||
// get midnight at the start of the next day for the timestamp
|
||||
timestamp.setDate(timestamp.getDate() + 1)
|
||||
timestamp.setHours(0, 0, 0, 0)
|
||||
|
||||
// if we have passed the threshold into the next day
|
||||
if (Date.now() > timestamp.getTime()) {
|
||||
// update the timestamp in the event -- send
|
||||
await recordEvent(event, { timestamp: Date.now() })
|
||||
return false
|
||||
} else {
|
||||
// still within the limited period -- don't send
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// no event present i.e. expired -- send
|
||||
await recordEvent(event, { timestamp: Date.now() })
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const eventKey = (event: RateLimitedEvent) => {
|
||||
let key = `${CacheKeys.EVENTS_RATE_LIMIT}:${event}`
|
||||
if (isPerApp(event)) {
|
||||
key = key + ":" + context.getAppId()
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
const readEvent = async (
|
||||
event: RateLimitedEvent
|
||||
): Promise<EventProperties | undefined> => {
|
||||
const key = eventKey(event)
|
||||
const result = await cache.get(key)
|
||||
return result as EventProperties
|
||||
}
|
||||
|
||||
const recordEvent = async (
|
||||
event: RateLimitedEvent,
|
||||
properties: EventProperties
|
||||
) => {
|
||||
const key = eventKey(event)
|
||||
const limit = RATE_LIMITS[event]
|
||||
let ttl
|
||||
switch (limit) {
|
||||
case RateLimit.CALENDAR_DAY: {
|
||||
ttl = TTL.ONE_DAY
|
||||
}
|
||||
}
|
||||
|
||||
await cache.store(key, properties, ttl)
|
||||
}
|
|
@ -0,0 +1,145 @@
|
|||
import "../../../../../tests/utilities/TestConfiguration"
|
||||
import PosthogProcessor from "../PosthogProcessor"
|
||||
import { Event, IdentityType, Hosting } from "@budibase/types"
|
||||
const tk = require("timekeeper")
|
||||
import * as cache from "../../../../cache/generic"
|
||||
import { CacheKeys } from "../../../../cache/generic"
|
||||
import * as context from "../../../../context"
|
||||
|
||||
const newIdentity = () => {
|
||||
return {
|
||||
id: "test",
|
||||
type: IdentityType.USER,
|
||||
hosting: Hosting.SELF,
|
||||
environment: "test",
|
||||
}
|
||||
}
|
||||
|
||||
describe("PosthogProcessor", () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
await cache.bustCache(
|
||||
`${CacheKeys.EVENTS_RATE_LIMIT}:${Event.SERVED_BUILDER}`
|
||||
)
|
||||
})
|
||||
|
||||
describe("processEvent", () => {
|
||||
it("processes event", async () => {
|
||||
const processor = new PosthogProcessor("test")
|
||||
|
||||
const identity = newIdentity()
|
||||
const properties = {}
|
||||
|
||||
await processor.processEvent(Event.APP_CREATED, identity, properties)
|
||||
|
||||
expect(processor.posthog.capture).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("honours exclusions", async () => {
|
||||
const processor = new PosthogProcessor("test")
|
||||
|
||||
const identity = newIdentity()
|
||||
const properties = {}
|
||||
|
||||
await processor.processEvent(Event.AUTH_SSO_UPDATED, identity, properties)
|
||||
expect(processor.posthog.capture).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
describe("rate limiting", () => {
|
||||
it("sends daily event once in same day", async () => {
|
||||
const processor = new PosthogProcessor("test")
|
||||
const identity = newIdentity()
|
||||
const properties = {}
|
||||
|
||||
tk.freeze(new Date(2022, 0, 1, 14, 0))
|
||||
await processor.processEvent(Event.SERVED_BUILDER, identity, properties)
|
||||
// go forward one hour
|
||||
tk.freeze(new Date(2022, 0, 1, 15, 0))
|
||||
await processor.processEvent(Event.SERVED_BUILDER, identity, properties)
|
||||
|
||||
expect(processor.posthog.capture).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("sends daily event once per unique day", async () => {
|
||||
const processor = new PosthogProcessor("test")
|
||||
const identity = newIdentity()
|
||||
const properties = {}
|
||||
|
||||
tk.freeze(new Date(2022, 0, 1, 14, 0))
|
||||
await processor.processEvent(Event.SERVED_BUILDER, identity, properties)
|
||||
// go forward into next day
|
||||
tk.freeze(new Date(2022, 0, 2, 9, 0))
|
||||
await processor.processEvent(Event.SERVED_BUILDER, identity, properties)
|
||||
// go forward into next day
|
||||
tk.freeze(new Date(2022, 0, 3, 5, 0))
|
||||
await processor.processEvent(Event.SERVED_BUILDER, identity, properties)
|
||||
// go forward one hour
|
||||
tk.freeze(new Date(2022, 0, 3, 6, 0))
|
||||
await processor.processEvent(Event.SERVED_BUILDER, identity, properties)
|
||||
|
||||
expect(processor.posthog.capture).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
it("sends event again after cache expires", async () => {
|
||||
const processor = new PosthogProcessor("test")
|
||||
const identity = newIdentity()
|
||||
const properties = {}
|
||||
|
||||
tk.freeze(new Date(2022, 0, 1, 14, 0))
|
||||
await processor.processEvent(Event.SERVED_BUILDER, identity, properties)
|
||||
|
||||
await cache.bustCache(
|
||||
`${CacheKeys.EVENTS_RATE_LIMIT}:${Event.SERVED_BUILDER}`
|
||||
)
|
||||
|
||||
tk.freeze(new Date(2022, 0, 1, 14, 0))
|
||||
await processor.processEvent(Event.SERVED_BUILDER, identity, properties)
|
||||
|
||||
expect(processor.posthog.capture).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it("sends per app events once per day per app", async () => {
|
||||
const processor = new PosthogProcessor("test")
|
||||
const identity = newIdentity()
|
||||
const properties = {}
|
||||
|
||||
const runAppEvents = async (appId: string) => {
|
||||
await context.doInAppContext(appId, async () => {
|
||||
tk.freeze(new Date(2022, 0, 1, 14, 0))
|
||||
await processor.processEvent(Event.SERVED_APP, identity, properties)
|
||||
await processor.processEvent(
|
||||
Event.SERVED_APP_PREVIEW,
|
||||
identity,
|
||||
properties
|
||||
)
|
||||
|
||||
// go forward one hour - should be ignored
|
||||
tk.freeze(new Date(2022, 0, 1, 15, 0))
|
||||
await processor.processEvent(Event.SERVED_APP, identity, properties)
|
||||
await processor.processEvent(
|
||||
Event.SERVED_APP_PREVIEW,
|
||||
identity,
|
||||
properties
|
||||
)
|
||||
|
||||
// go forward into next day
|
||||
tk.freeze(new Date(2022, 0, 2, 9, 0))
|
||||
|
||||
await processor.processEvent(Event.SERVED_APP, identity, properties)
|
||||
await processor.processEvent(
|
||||
Event.SERVED_APP_PREVIEW,
|
||||
identity,
|
||||
properties
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
await runAppEvents("app_1")
|
||||
expect(processor.posthog.capture).toHaveBeenCalledTimes(4)
|
||||
|
||||
await runAppEvents("app_2")
|
||||
expect(processor.posthog.capture).toHaveBeenCalledTimes(8)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -20,12 +20,6 @@ export async function downgraded(license: License) {
|
|||
await publishEvent(Event.LICENSE_DOWNGRADED, properties)
|
||||
}
|
||||
|
||||
// TODO
|
||||
export async function updated(license: License) {
|
||||
const properties: LicenseUpdatedEvent = {}
|
||||
await publishEvent(Event.LICENSE_UPDATED, properties)
|
||||
}
|
||||
|
||||
// TODO
|
||||
export async function activated(license: License) {
|
||||
const properties: LicenseActivatedEvent = {}
|
||||
|
|
|
@ -7,22 +7,26 @@ import {
|
|||
AppServedEvent,
|
||||
} from "@budibase/types"
|
||||
|
||||
export async function servedBuilder() {
|
||||
const properties: BuilderServedEvent = {}
|
||||
export async function servedBuilder(timezone: string) {
|
||||
const properties: BuilderServedEvent = {
|
||||
timezone,
|
||||
}
|
||||
await publishEvent(Event.SERVED_BUILDER, properties)
|
||||
}
|
||||
|
||||
export async function servedApp(app: App) {
|
||||
export async function servedApp(app: App, timezone: string) {
|
||||
const properties: AppServedEvent = {
|
||||
appVersion: app.version,
|
||||
timezone,
|
||||
}
|
||||
await publishEvent(Event.SERVED_APP, properties)
|
||||
}
|
||||
|
||||
export async function servedAppPreview(app: App) {
|
||||
export async function servedAppPreview(app: App, timezone: string) {
|
||||
const properties: AppPreviewServedEvent = {
|
||||
appId: app.appId,
|
||||
appVersion: app.version,
|
||||
timezone,
|
||||
}
|
||||
await publishEvent(Event.SERVED_APP_PREVIEW, properties)
|
||||
}
|
||||
|
|
|
@ -50,4 +50,5 @@ exports.getTenantFeatureFlags = tenantId => {
|
|||
exports.FeatureFlag = {
|
||||
LICENSING: "LICENSING",
|
||||
GOOGLE_SHEETS: "GOOGLE_SHEETS",
|
||||
USER_GROUPS: "USER_GROUPS",
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import errors from "./errors"
|
||||
|
||||
const errorClasses = errors.errors
|
||||
import * as events from "./events"
|
||||
import * as migrations from "./migrations"
|
||||
|
@ -9,12 +10,13 @@ import * as installation from "./installation"
|
|||
import env from "./environment"
|
||||
import tenancy from "./tenancy"
|
||||
import featureFlags from "./featureFlags"
|
||||
import sessions from "./security/sessions"
|
||||
import * as sessions from "./security/sessions"
|
||||
import deprovisioning from "./context/deprovision"
|
||||
import auth from "./auth"
|
||||
import constants from "./constants"
|
||||
import * as dbConstants from "./db/constants"
|
||||
import logging from "./logging"
|
||||
import pino from "./pino"
|
||||
|
||||
// mimic the outer package exports
|
||||
import * as db from "./pkg/db"
|
||||
|
@ -53,6 +55,7 @@ const core = {
|
|||
errors,
|
||||
logging,
|
||||
roles,
|
||||
...pino,
|
||||
...errorClasses,
|
||||
}
|
||||
|
||||
|
|
|
@ -15,11 +15,22 @@ export function logAlert(message: string, e?: any) {
|
|||
console.error(`bb-alert: ${message} ${errorJson}`)
|
||||
}
|
||||
|
||||
export function logAlertWithInfo(
|
||||
message: string,
|
||||
db: string,
|
||||
id: string,
|
||||
error: any
|
||||
) {
|
||||
message = `${message} - db: ${db} - doc: ${id} - error: `
|
||||
logAlert(message, error)
|
||||
}
|
||||
|
||||
export function logWarn(message: string) {
|
||||
console.warn(`bb-warn: ${message}`)
|
||||
}
|
||||
|
||||
export default {
|
||||
logAlert,
|
||||
logAlertWithInfo,
|
||||
logWarn,
|
||||
}
|
||||
|
|
|
@ -1,28 +1,39 @@
|
|||
const { Cookies, Headers } = require("../constants")
|
||||
const { getCookie, clearCookie, openJwt } = require("../utils")
|
||||
const { getUser } = require("../cache/user")
|
||||
const { getSession, updateSessionTTL } = require("../security/sessions")
|
||||
const { buildMatcherRegex, matches } = require("./matchers")
|
||||
const env = require("../environment")
|
||||
const { SEPARATOR } = require("../db/constants")
|
||||
const { ViewNames } = require("../db/utils")
|
||||
const { queryGlobalView } = require("../db/views")
|
||||
const { getGlobalDB, doInTenant } = require("../tenancy")
|
||||
const { decrypt } = require("../security/encryption")
|
||||
import { Cookies, Headers } from "../constants"
|
||||
import { getCookie, clearCookie, openJwt } from "../utils"
|
||||
import { getUser } from "../cache/user"
|
||||
import { getSession, updateSessionTTL } from "../security/sessions"
|
||||
import { buildMatcherRegex, matches } from "./matchers"
|
||||
import { SEPARATOR } from "../db/constants"
|
||||
import { ViewName } from "../db/utils"
|
||||
import { queryGlobalView } from "../db/views"
|
||||
import { getGlobalDB, doInTenant } from "../tenancy"
|
||||
import { decrypt } from "../security/encryption"
|
||||
const identity = require("../context/identity")
|
||||
const env = require("../environment")
|
||||
|
||||
function finalise(
|
||||
ctx,
|
||||
{ authenticated, user, internal, version, publicEndpoint } = {}
|
||||
) {
|
||||
ctx.publicEndpoint = publicEndpoint || false
|
||||
ctx.isAuthenticated = authenticated || false
|
||||
ctx.user = user
|
||||
ctx.internal = internal || false
|
||||
ctx.version = version
|
||||
const ONE_MINUTE = env.SESSION_UPDATE_PERIOD || 60 * 1000
|
||||
|
||||
interface FinaliseOpts {
|
||||
authenticated?: boolean
|
||||
internal?: boolean
|
||||
publicEndpoint?: boolean
|
||||
version?: string
|
||||
user?: any
|
||||
}
|
||||
|
||||
async function checkApiKey(apiKey, populateUser) {
|
||||
function timeMinusOneMinute() {
|
||||
return new Date(Date.now() - ONE_MINUTE).toISOString()
|
||||
}
|
||||
|
||||
function finalise(ctx: any, opts: FinaliseOpts = {}) {
|
||||
ctx.publicEndpoint = opts.publicEndpoint || false
|
||||
ctx.isAuthenticated = opts.authenticated || false
|
||||
ctx.user = opts.user
|
||||
ctx.internal = opts.internal || false
|
||||
ctx.version = opts.version
|
||||
}
|
||||
|
||||
async function checkApiKey(apiKey: string, populateUser?: Function) {
|
||||
if (apiKey === env.INTERNAL_API_KEY) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
@ -32,7 +43,7 @@ async function checkApiKey(apiKey, populateUser) {
|
|||
const db = getGlobalDB()
|
||||
// api key is encrypted in the database
|
||||
const userId = await queryGlobalView(
|
||||
ViewNames.BY_API_KEY,
|
||||
ViewName.BY_API_KEY,
|
||||
{
|
||||
key: apiKey,
|
||||
},
|
||||
|
@ -56,10 +67,12 @@ async function checkApiKey(apiKey, populateUser) {
|
|||
*/
|
||||
module.exports = (
|
||||
noAuthPatterns = [],
|
||||
opts = { publicAllowed: false, populateUser: null }
|
||||
opts: { publicAllowed: boolean; populateUser?: Function } = {
|
||||
publicAllowed: false,
|
||||
}
|
||||
) => {
|
||||
const noAuthOptions = noAuthPatterns ? buildMatcherRegex(noAuthPatterns) : []
|
||||
return async (ctx, next) => {
|
||||
return async (ctx: any, next: any) => {
|
||||
let publicEndpoint = false
|
||||
const version = ctx.request.headers[Headers.API_VER]
|
||||
// the path is not authenticated
|
||||
|
@ -71,45 +84,40 @@ module.exports = (
|
|||
// check the actual user is authenticated first, try header or cookie
|
||||
const headerToken = ctx.request.headers[Headers.TOKEN]
|
||||
const authCookie = getCookie(ctx, Cookies.Auth) || openJwt(headerToken)
|
||||
const apiKey = ctx.request.headers[Headers.API_KEY]
|
||||
const tenantId = ctx.request.headers[Headers.TENANT_ID]
|
||||
let authenticated = false,
|
||||
user = null,
|
||||
internal = false
|
||||
if (authCookie) {
|
||||
let error = null
|
||||
if (authCookie && !apiKey) {
|
||||
const sessionId = authCookie.sessionId
|
||||
const userId = authCookie.userId
|
||||
|
||||
const session = await getSession(userId, sessionId)
|
||||
if (!session) {
|
||||
error = "No session found"
|
||||
} else {
|
||||
try {
|
||||
if (opts && opts.populateUser) {
|
||||
user = await getUser(
|
||||
userId,
|
||||
session.tenantId,
|
||||
opts.populateUser(ctx)
|
||||
)
|
||||
} else {
|
||||
user = await getUser(userId, session.tenantId)
|
||||
}
|
||||
user.csrfToken = session.csrfToken
|
||||
authenticated = true
|
||||
} catch (err) {
|
||||
error = err
|
||||
let session
|
||||
try {
|
||||
// getting session handles error checking (if session exists etc)
|
||||
session = await getSession(userId, sessionId)
|
||||
if (opts && opts.populateUser) {
|
||||
user = await getUser(
|
||||
userId,
|
||||
session.tenantId,
|
||||
opts.populateUser(ctx)
|
||||
)
|
||||
} else {
|
||||
user = await getUser(userId, session.tenantId)
|
||||
}
|
||||
}
|
||||
if (error) {
|
||||
console.error("Auth Error", error)
|
||||
user.csrfToken = session.csrfToken
|
||||
if (session?.lastAccessedAt < timeMinusOneMinute()) {
|
||||
// make sure we denote that the session is still in use
|
||||
await updateSessionTTL(session)
|
||||
}
|
||||
authenticated = true
|
||||
} catch (err: any) {
|
||||
authenticated = false
|
||||
console.error("Auth Error", err?.message || err)
|
||||
// remove the cookie as the user does not exist anymore
|
||||
clearCookie(ctx, Cookies.Auth)
|
||||
} else {
|
||||
// make sure we denote that the session is still in use
|
||||
await updateSessionTTL(session)
|
||||
}
|
||||
}
|
||||
const apiKey = ctx.request.headers[Headers.API_KEY]
|
||||
const tenantId = ctx.request.headers[Headers.TENANT_ID]
|
||||
// this is an internal request, no user made it
|
||||
if (!authenticated && apiKey) {
|
||||
const populateUser = opts.populateUser ? opts.populateUser(ctx) : null
|
||||
|
@ -142,7 +150,7 @@ module.exports = (
|
|||
} else {
|
||||
return next()
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
// invalid token, clear the cookie
|
||||
if (err && err.name === "JsonWebTokenError") {
|
||||
clearCookie(ctx, Cookies.Auth)
|
|
@ -10,6 +10,8 @@ const internalApi = require("./internalApi")
|
|||
const datasourceGoogle = require("./passport/datasource/google")
|
||||
const csrf = require("./csrf")
|
||||
const adminOnly = require("./adminOnly")
|
||||
const builderOrAdmin = require("./builderOrAdmin")
|
||||
const builderOnly = require("./builderOnly")
|
||||
const joiValidator = require("./joi-validator")
|
||||
module.exports = {
|
||||
google,
|
||||
|
@ -27,5 +29,7 @@ module.exports = {
|
|||
},
|
||||
csrf,
|
||||
adminOnly,
|
||||
builderOnly,
|
||||
builderOrAdmin,
|
||||
joiValidator,
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
const Joi = require("joi")
|
||||
|
||||
function validate(schema, property) {
|
||||
// Return a Koa middleware function
|
||||
return (ctx, next) => {
|
||||
|
@ -10,6 +12,12 @@ function validate(schema, property) {
|
|||
} else if (ctx.request[property] != null) {
|
||||
params = ctx.request[property]
|
||||
}
|
||||
|
||||
schema = schema.append({
|
||||
createdAt: Joi.any().optional(),
|
||||
updatedAt: Joi.any().optional(),
|
||||
})
|
||||
|
||||
const { error } = schema.validate(params)
|
||||
if (error) {
|
||||
ctx.throw(400, `Invalid ${property} - ${error.message}`)
|
||||
|
|
|
@ -37,4 +37,8 @@ export const DEFINITIONS: MigrationDefinition[] = [
|
|||
type: MigrationType.INSTALLATION,
|
||||
name: MigrationName.EVENT_INSTALLATION_BACKFILL,
|
||||
},
|
||||
{
|
||||
type: MigrationType.GLOBAL,
|
||||
name: MigrationName.GLOBAL_INFO_SYNC_USERS,
|
||||
},
|
||||
]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { DEFAULT_TENANT_ID } from "../constants"
|
||||
import { doWithDB } from "../db"
|
||||
import { DocumentTypes, StaticDatabases } from "../db/constants"
|
||||
import { DocumentType, StaticDatabases } from "../db/constants"
|
||||
import { getAllApps } from "../db/utils"
|
||||
import environment from "../environment"
|
||||
import {
|
||||
|
@ -21,10 +21,10 @@ import {
|
|||
export const getMigrationsDoc = async (db: any) => {
|
||||
// get the migrations doc
|
||||
try {
|
||||
return await db.get(DocumentTypes.MIGRATIONS)
|
||||
return await db.get(DocumentType.MIGRATIONS)
|
||||
} catch (err: any) {
|
||||
if (err.status && err.status === 404) {
|
||||
return { _id: DocumentTypes.MIGRATIONS }
|
||||
return { _id: DocumentType.MIGRATIONS }
|
||||
} else {
|
||||
console.error(err)
|
||||
throw err
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
const env = require("./environment")
|
||||
|
||||
exports.pinoSettings = () => ({
|
||||
prettyPrint: {
|
||||
levelFirst: true,
|
||||
},
|
||||
level: env.LOG_LEVEL || "error",
|
||||
autoLogging: {
|
||||
ignore: req => req.url.includes("/health"),
|
||||
},
|
||||
})
|
|
@ -3,7 +3,7 @@ const { BUILTIN_PERMISSION_IDS, PermissionLevels } = require("./permissions")
|
|||
const {
|
||||
generateRoleID,
|
||||
getRoleParams,
|
||||
DocumentTypes,
|
||||
DocumentType,
|
||||
SEPARATOR,
|
||||
} = require("../db/utils")
|
||||
const { getAppDB } = require("../context")
|
||||
|
@ -203,15 +203,24 @@ exports.getAllRoles = async appId => {
|
|||
if (appId) {
|
||||
return doWithDB(appId, internal)
|
||||
} else {
|
||||
return internal(getAppDB())
|
||||
let appDB
|
||||
try {
|
||||
appDB = getAppDB()
|
||||
} catch (error) {
|
||||
// We don't have any apps, so we'll just use the built-in roles
|
||||
}
|
||||
return internal(appDB)
|
||||
}
|
||||
async function internal(db) {
|
||||
const body = await db.allDocs(
|
||||
getRoleParams(null, {
|
||||
include_docs: true,
|
||||
})
|
||||
)
|
||||
let roles = body.rows.map(row => row.doc)
|
||||
let roles = []
|
||||
if (db) {
|
||||
const body = await db.allDocs(
|
||||
getRoleParams(null, {
|
||||
include_docs: true,
|
||||
})
|
||||
)
|
||||
roles = body.rows.map(row => row.doc)
|
||||
}
|
||||
const builtinRoles = exports.getBuiltinRoles()
|
||||
|
||||
// need to combine builtin with any DB record of them (for sake of permissions)
|
||||
|
@ -329,7 +338,7 @@ class AccessController {
|
|||
* Adds the "role_" for builtin role IDs which are to be written to the DB (for permissions).
|
||||
*/
|
||||
exports.getDBRoleID = roleId => {
|
||||
if (roleId.startsWith(DocumentTypes.ROLE)) {
|
||||
if (roleId.startsWith(DocumentType.ROLE)) {
|
||||
return roleId
|
||||
}
|
||||
return generateRoleID(roleId)
|
||||
|
@ -340,8 +349,8 @@ exports.getDBRoleID = roleId => {
|
|||
*/
|
||||
exports.getExternalRoleID = roleId => {
|
||||
// for built in roles we want to remove the DB role ID element (role_)
|
||||
if (roleId.startsWith(DocumentTypes.ROLE) && isBuiltin(roleId)) {
|
||||
return roleId.split(`${DocumentTypes.ROLE}${SEPARATOR}`)[1]
|
||||
if (roleId.startsWith(DocumentType.ROLE) && isBuiltin(roleId)) {
|
||||
return roleId.split(`${DocumentType.ROLE}${SEPARATOR}`)[1]
|
||||
}
|
||||
return roleId
|
||||
}
|
||||
|
|
|
@ -1,95 +0,0 @@
|
|||
const redis = require("../redis/init")
|
||||
const { v4: uuidv4 } = require("uuid")
|
||||
|
||||
// a week in seconds
|
||||
const EXPIRY_SECONDS = 86400 * 7
|
||||
|
||||
async function getSessionsForUser(userId) {
|
||||
const client = await redis.getSessionClient()
|
||||
const sessions = await client.scan(userId)
|
||||
return sessions.map(session => session.value)
|
||||
}
|
||||
|
||||
function makeSessionID(userId, sessionId) {
|
||||
return `${userId}/${sessionId}`
|
||||
}
|
||||
|
||||
async function invalidateSessions(userId, sessionIds = null) {
|
||||
try {
|
||||
let sessions = []
|
||||
|
||||
// If no sessionIds, get all the sessions for the user
|
||||
if (!sessionIds) {
|
||||
sessions = await getSessionsForUser(userId)
|
||||
sessions.forEach(
|
||||
session =>
|
||||
(session.key = makeSessionID(session.userId, session.sessionId))
|
||||
)
|
||||
} else {
|
||||
// use the passed array of sessionIds
|
||||
sessions = Array.isArray(sessionIds) ? sessionIds : [sessionIds]
|
||||
sessions = sessions.map(sessionId => ({
|
||||
key: makeSessionID(userId, sessionId),
|
||||
}))
|
||||
}
|
||||
|
||||
const client = await redis.getSessionClient()
|
||||
const promises = []
|
||||
for (let session of sessions) {
|
||||
promises.push(client.delete(session.key))
|
||||
}
|
||||
await Promise.all(promises)
|
||||
} catch (err) {
|
||||
console.error(`Error invalidating sessions: ${err}`)
|
||||
}
|
||||
}
|
||||
|
||||
exports.createASession = async (userId, session) => {
|
||||
// invalidate all other sessions
|
||||
await invalidateSessions(userId)
|
||||
|
||||
const client = await redis.getSessionClient()
|
||||
const sessionId = session.sessionId
|
||||
if (!session.csrfToken) {
|
||||
session.csrfToken = uuidv4()
|
||||
}
|
||||
session = {
|
||||
createdAt: new Date().toISOString(),
|
||||
lastAccessedAt: new Date().toISOString(),
|
||||
...session,
|
||||
userId,
|
||||
}
|
||||
await client.store(makeSessionID(userId, sessionId), session, EXPIRY_SECONDS)
|
||||
}
|
||||
|
||||
exports.updateSessionTTL = async session => {
|
||||
const client = await redis.getSessionClient()
|
||||
const key = makeSessionID(session.userId, session.sessionId)
|
||||
session.lastAccessedAt = new Date().toISOString()
|
||||
await client.store(key, session, EXPIRY_SECONDS)
|
||||
}
|
||||
|
||||
exports.endSession = async (userId, sessionId) => {
|
||||
const client = await redis.getSessionClient()
|
||||
await client.delete(makeSessionID(userId, sessionId))
|
||||
}
|
||||
|
||||
exports.getSession = async (userId, sessionId) => {
|
||||
try {
|
||||
const client = await redis.getSessionClient()
|
||||
return client.get(makeSessionID(userId, sessionId))
|
||||
} catch (err) {
|
||||
// if can't get session don't error, just don't return anything
|
||||
console.error(err)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
exports.getAllSessions = async () => {
|
||||
const client = await redis.getSessionClient()
|
||||
const sessions = await client.scan()
|
||||
return sessions.map(session => session.value)
|
||||
}
|
||||
|
||||
exports.getUserSessions = getSessionsForUser
|
||||
exports.invalidateSessions = invalidateSessions
|
|
@ -0,0 +1,119 @@
|
|||
const redis = require("../redis/init")
|
||||
const { v4: uuidv4 } = require("uuid")
|
||||
const { logWarn } = require("../logging")
|
||||
const env = require("../environment")
|
||||
|
||||
interface Session {
|
||||
key: string
|
||||
userId: string
|
||||
sessionId: string
|
||||
lastAccessedAt: string
|
||||
createdAt: string
|
||||
csrfToken?: string
|
||||
value: string
|
||||
}
|
||||
|
||||
type SessionKey = { key: string }[]
|
||||
|
||||
// a week in seconds
|
||||
const EXPIRY_SECONDS = 86400 * 7
|
||||
|
||||
function makeSessionID(userId: string, sessionId: string) {
|
||||
return `${userId}/${sessionId}`
|
||||
}
|
||||
|
||||
export async function getSessionsForUser(userId: string) {
|
||||
if (!userId) {
|
||||
console.trace("Cannot get sessions for undefined userId")
|
||||
return []
|
||||
}
|
||||
const client = await redis.getSessionClient()
|
||||
const sessions = await client.scan(userId)
|
||||
return sessions.map((session: Session) => session.value)
|
||||
}
|
||||
|
||||
export async function invalidateSessions(
|
||||
userId: string,
|
||||
opts: { sessionIds?: string[]; reason?: string } = {}
|
||||
) {
|
||||
try {
|
||||
const reason = opts?.reason || "unknown"
|
||||
let sessionIds: string[] = opts.sessionIds || []
|
||||
let sessions: SessionKey
|
||||
|
||||
// If no sessionIds, get all the sessions for the user
|
||||
if (sessionIds.length === 0) {
|
||||
sessions = await getSessionsForUser(userId)
|
||||
sessions.forEach(
|
||||
(session: any) =>
|
||||
(session.key = makeSessionID(session.userId, session.sessionId))
|
||||
)
|
||||
} else {
|
||||
// use the passed array of sessionIds
|
||||
sessionIds = Array.isArray(sessionIds) ? sessionIds : [sessionIds]
|
||||
sessions = sessionIds.map((sessionId: string) => ({
|
||||
key: makeSessionID(userId, sessionId),
|
||||
}))
|
||||
}
|
||||
|
||||
if (sessions && sessions.length > 0) {
|
||||
const client = await redis.getSessionClient()
|
||||
const promises = []
|
||||
for (let session of sessions) {
|
||||
promises.push(client.delete(session.key))
|
||||
}
|
||||
if (!env.isTest()) {
|
||||
logWarn(
|
||||
`Invalidating sessions for ${userId} (reason: ${reason}) - ${sessions
|
||||
.map(session => session.key)
|
||||
.join(", ")}`
|
||||
)
|
||||
}
|
||||
await Promise.all(promises)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error invalidating sessions: ${err}`)
|
||||
}
|
||||
}
|
||||
|
||||
export async function createASession(userId: string, session: Session) {
|
||||
// invalidate all other sessions
|
||||
await invalidateSessions(userId, { reason: "creation" })
|
||||
|
||||
const client = await redis.getSessionClient()
|
||||
const sessionId = session.sessionId
|
||||
if (!session.csrfToken) {
|
||||
session.csrfToken = uuidv4()
|
||||
}
|
||||
session = {
|
||||
...session,
|
||||
createdAt: new Date().toISOString(),
|
||||
lastAccessedAt: new Date().toISOString(),
|
||||
userId,
|
||||
}
|
||||
await client.store(makeSessionID(userId, sessionId), session, EXPIRY_SECONDS)
|
||||
}
|
||||
|
||||
export async function updateSessionTTL(session: Session) {
|
||||
const client = await redis.getSessionClient()
|
||||
const key = makeSessionID(session.userId, session.sessionId)
|
||||
session.lastAccessedAt = new Date().toISOString()
|
||||
await client.store(key, session, EXPIRY_SECONDS)
|
||||
}
|
||||
|
||||
export async function endSession(userId: string, sessionId: string) {
|
||||
const client = await redis.getSessionClient()
|
||||
await client.delete(makeSessionID(userId, sessionId))
|
||||
}
|
||||
|
||||
export async function getSession(userId: string, sessionId: string) {
|
||||
if (!userId || !sessionId) {
|
||||
throw new Error(`Invalid session details - ${userId} - ${sessionId}`)
|
||||
}
|
||||
const client = await redis.getSessionClient()
|
||||
const session = await client.get(makeSessionID(userId, sessionId))
|
||||
if (!session) {
|
||||
throw new Error(`Session not found - ${userId} - ${sessionId}`)
|
||||
}
|
||||
return session
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import * as sessions from "../sessions"
|
||||
|
||||
describe("sessions", () => {
|
||||
describe("getSessionsForUser", () => {
|
||||
it("returns empty when user is undefined", async () => {
|
||||
// @ts-ignore - allow the undefined to be passed
|
||||
const results = await sessions.getSessionsForUser(undefined)
|
||||
|
||||
expect(results).toStrictEqual([])
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,5 +1,5 @@
|
|||
const {
|
||||
ViewNames,
|
||||
ViewName,
|
||||
getUsersByAppParams,
|
||||
getProdAppID,
|
||||
generateAppUserID,
|
||||
|
@ -18,7 +18,7 @@ exports.getGlobalUserByEmail = async email => {
|
|||
throw "Must supply an email address to view"
|
||||
}
|
||||
|
||||
return await queryGlobalView(ViewNames.USER_BY_EMAIL, {
|
||||
return await queryGlobalView(ViewName.USER_BY_EMAIL, {
|
||||
key: email.toLowerCase(),
|
||||
include_docs: true,
|
||||
})
|
||||
|
@ -32,7 +32,7 @@ exports.searchGlobalUsersByApp = async (appId, opts) => {
|
|||
include_docs: true,
|
||||
})
|
||||
params.startkey = opts && opts.startkey ? opts.startkey : params.startkey
|
||||
let response = await queryGlobalView(ViewNames.USER_BY_APP, params)
|
||||
let response = await queryGlobalView(ViewName.USER_BY_APP, params)
|
||||
if (!response) {
|
||||
response = []
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ exports.searchGlobalUsersByEmail = async (email, opts) => {
|
|||
const lcEmail = email.toLowerCase()
|
||||
// handle if passing up startkey for pagination
|
||||
const startkey = opts && opts.startkey ? opts.startkey : lcEmail
|
||||
let response = await queryGlobalView(ViewNames.USER_BY_EMAIL, {
|
||||
let response = await queryGlobalView(ViewName.USER_BY_EMAIL, {
|
||||
...opts,
|
||||
startkey,
|
||||
endkey: `${lcEmail}${UNICODE_MAX}`,
|
||||
|
|
|
@ -1,20 +1,18 @@
|
|||
const {
|
||||
DocumentTypes,
|
||||
SEPARATOR,
|
||||
ViewNames,
|
||||
getAllApps,
|
||||
} = require("./db/utils")
|
||||
const { DocumentType, SEPARATOR, ViewName, getAllApps } = require("./db/utils")
|
||||
const jwt = require("jsonwebtoken")
|
||||
const { options } = require("./middleware/passport/jwt")
|
||||
const { queryGlobalView } = require("./db/views")
|
||||
const { Headers, Cookies, MAX_VALID_DATE } = require("./constants")
|
||||
const env = require("./environment")
|
||||
const userCache = require("./cache/user")
|
||||
const { getUserSessions, invalidateSessions } = require("./security/sessions")
|
||||
const {
|
||||
getSessionsForUser,
|
||||
invalidateSessions,
|
||||
} = require("./security/sessions")
|
||||
const events = require("./events")
|
||||
const tenancy = require("./tenancy")
|
||||
|
||||
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
|
||||
const APP_PREFIX = DocumentType.APP + SEPARATOR
|
||||
const PROD_APP_PREFIX = "/app/"
|
||||
|
||||
function confirmAppId(possibleAppId) {
|
||||
|
@ -151,7 +149,7 @@ exports.isClient = ctx => {
|
|||
}
|
||||
|
||||
const getBuilders = async () => {
|
||||
const builders = await queryGlobalView(ViewNames.USER_BY_BUILDERS, {
|
||||
const builders = await queryGlobalView(ViewName.USER_BY_BUILDERS, {
|
||||
include_docs: false,
|
||||
})
|
||||
|
||||
|
@ -178,7 +176,7 @@ exports.platformLogout = async ({ ctx, userId, keepActiveSession }) => {
|
|||
if (!ctx) throw new Error("Koa context must be supplied to logout.")
|
||||
|
||||
const currentSession = exports.getCookie(ctx, Cookies.Auth)
|
||||
let sessions = await getUserSessions(userId)
|
||||
let sessions = await getSessionsForUser(userId)
|
||||
|
||||
if (keepActiveSession) {
|
||||
sessions = sessions.filter(
|
||||
|
@ -190,10 +188,8 @@ exports.platformLogout = async ({ ctx, userId, keepActiveSession }) => {
|
|||
exports.clearCookie(ctx, Cookies.CurrentApp)
|
||||
}
|
||||
|
||||
await invalidateSessions(
|
||||
userId,
|
||||
sessions.map(({ sessionId }) => sessionId)
|
||||
)
|
||||
const sessionIds = sessions.map(({ sessionId }) => sessionId)
|
||||
await invalidateSessions(userId, { sessionIds, reason: "logout" })
|
||||
await events.auth.logout()
|
||||
await userCache.invalidateUser(userId)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
const posthog = require("./posthog")
|
||||
const events = require("./events")
|
||||
const date = require("./date")
|
||||
|
||||
module.exports = {
|
||||
posthog,
|
||||
date,
|
||||
events,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
jest.mock("posthog-node", () => {
|
||||
return jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
capture: jest.fn(),
|
||||
}
|
||||
})
|
||||
})
|
|
@ -291,6 +291,18 @@
|
|||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
||||
|
||||
"@hapi/hoek@^9.0.0":
|
||||
version "9.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb"
|
||||
integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==
|
||||
|
||||
"@hapi/topo@^5.0.0":
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012"
|
||||
integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==
|
||||
dependencies:
|
||||
"@hapi/hoek" "^9.0.0"
|
||||
|
||||
"@istanbuljs/load-nyc-config@^1.0.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
|
||||
|
@ -539,6 +551,23 @@
|
|||
koa "^2.13.4"
|
||||
node-mocks-http "^1.5.8"
|
||||
|
||||
"@sideway/address@^4.1.3":
|
||||
version "4.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.4.tgz#03dccebc6ea47fdc226f7d3d1ad512955d4783f0"
|
||||
integrity sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==
|
||||
dependencies:
|
||||
"@hapi/hoek" "^9.0.0"
|
||||
|
||||
"@sideway/formula@^3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c"
|
||||
integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==
|
||||
|
||||
"@sideway/pinpoint@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df"
|
||||
integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==
|
||||
|
||||
"@sindresorhus/is@^0.14.0":
|
||||
version "0.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
|
||||
|
@ -3193,6 +3222,17 @@ jmespath@0.15.0:
|
|||
resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217"
|
||||
integrity sha512-+kHj8HXArPfpPEKGLZ+kB5ONRTCiGQXo8RQYL0hH8t6pWXUBBK5KkkQmTNOwKK4LEsd0yTsgtjJVm4UBSZea4w==
|
||||
|
||||
joi@17.6.0:
|
||||
version "17.6.0"
|
||||
resolved "https://registry.yarnpkg.com/joi/-/joi-17.6.0.tgz#0bb54f2f006c09a96e75ce687957bd04290054b2"
|
||||
integrity sha512-OX5dG6DTbcr/kbMFj0KGYxuew69HPcAE3K/sZpEV2nP6e/j/C0HV+HNiBPCASxdx5T7DMoa0s8UeHWMnb6n2zw==
|
||||
dependencies:
|
||||
"@hapi/hoek" "^9.0.0"
|
||||
"@hapi/topo" "^5.0.0"
|
||||
"@sideway/address" "^4.1.3"
|
||||
"@sideway/formula" "^3.0.0"
|
||||
"@sideway/pinpoint" "^2.0.0"
|
||||
|
||||
join-component@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/join-component/-/join-component-1.1.0.tgz#b8417b750661a392bee2c2537c68b2a9d4977cd5"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/bbui",
|
||||
"description": "A UI solution used in the different Budibase projects.",
|
||||
"version": "1.1.29-alpha.2",
|
||||
"version": "1.2.39-alpha.5",
|
||||
"license": "MPL-2.0",
|
||||
"svelte": "src/index.js",
|
||||
"module": "dist/bbui.es.js",
|
||||
|
@ -38,7 +38,7 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
|
||||
"@budibase/string-templates": "^1.1.29-alpha.2",
|
||||
"@budibase/string-templates": "1.2.39-alpha.5",
|
||||
"@spectrum-css/actionbutton": "^1.0.1",
|
||||
"@spectrum-css/actiongroup": "^1.0.1",
|
||||
"@spectrum-css/avatar": "^3.0.2",
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
export let appendTo = undefined
|
||||
export let timeOnly = false
|
||||
export let ignoreTimezones = false
|
||||
export let time24hr = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const flatpickrId = `${uuid()}-wrapper`
|
||||
|
@ -37,6 +38,7 @@
|
|||
enableTime: timeOnly || enableTime || false,
|
||||
noCalendar: timeOnly || false,
|
||||
altInput: true,
|
||||
time_24hr: time24hr || false,
|
||||
altFormat: timeOnly ? "H:i" : enableTime ? "F j Y, H:i" : "F j, Y",
|
||||
wrap: true,
|
||||
appendTo,
|
||||
|
@ -49,6 +51,12 @@
|
|||
},
|
||||
}
|
||||
|
||||
$: redrawOptions = {
|
||||
timeOnly,
|
||||
enableTime,
|
||||
time24hr,
|
||||
}
|
||||
|
||||
const handleChange = event => {
|
||||
const [dates] = event.detail
|
||||
const noTimezone = enableTime && !timeOnly && ignoreTimezones
|
||||
|
@ -142,7 +150,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
{#key timeOnly}
|
||||
{#key redrawOptions}
|
||||
<Flatpickr
|
||||
bind:flatpickr
|
||||
value={parseDate(value)}
|
||||
|
|
|
@ -115,6 +115,16 @@
|
|||
class:is-disabled={disabled}
|
||||
class:is-focused={focus}
|
||||
>
|
||||
{#if error}
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<use xlink:href="#spectrum-icon-18-Alert" />
|
||||
</svg>
|
||||
{/if}
|
||||
|
||||
<input
|
||||
{id}
|
||||
on:click
|
||||
|
|
|
@ -8,13 +8,13 @@
|
|||
import Icon from "../../Icon/Icon.svelte"
|
||||
import StatusLight from "../../StatusLight/StatusLight.svelte"
|
||||
import Detail from "../../Typography/Detail.svelte"
|
||||
import Search from "./Search.svelte"
|
||||
|
||||
export let primaryLabel = ""
|
||||
export let primaryValue = null
|
||||
export let id = null
|
||||
export let placeholder = "Choose an option or type"
|
||||
export let disabled = false
|
||||
export let readonly = false
|
||||
export let updateOnChange = true
|
||||
export let error = null
|
||||
export let secondaryOptions = []
|
||||
|
@ -22,7 +22,6 @@
|
|||
export let secondaryFieldText = ""
|
||||
export let secondaryFieldIcon = ""
|
||||
export let secondaryFieldColour = ""
|
||||
export let getPrimaryOptionLabel = option => option
|
||||
export let getPrimaryOptionValue = option => option
|
||||
export let getPrimaryOptionColour = () => null
|
||||
export let getPrimaryOptionIcon = () => null
|
||||
|
@ -35,6 +34,7 @@
|
|||
export let isOptionSelected = () => false
|
||||
export let isPlaceholder = false
|
||||
export let placeholderOption = null
|
||||
export let showClearIcon = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let primaryOpen = false
|
||||
|
@ -43,29 +43,18 @@
|
|||
let searchTerm = null
|
||||
|
||||
$: groupTitles = Object.keys(primaryOptions)
|
||||
$: filteredOptions = getFilteredOptions(
|
||||
primaryOptions,
|
||||
searchTerm,
|
||||
getPrimaryOptionLabel
|
||||
)
|
||||
let iconData
|
||||
/*
|
||||
$: iconData = primaryOptions?.find(x => {
|
||||
return x.name === primaryFieldText
|
||||
})
|
||||
*/
|
||||
|
||||
const updateSearch = e => {
|
||||
dispatch("search", e.detail)
|
||||
}
|
||||
|
||||
const updateValue = newValue => {
|
||||
if (readonly) {
|
||||
return
|
||||
}
|
||||
dispatch("change", newValue)
|
||||
}
|
||||
|
||||
const onClickSecondary = () => {
|
||||
dispatch("click")
|
||||
if (readonly) {
|
||||
return
|
||||
}
|
||||
secondaryOpen = true
|
||||
}
|
||||
|
||||
|
@ -85,38 +74,19 @@
|
|||
}
|
||||
|
||||
const onBlur = event => {
|
||||
if (readonly) {
|
||||
return
|
||||
}
|
||||
focus = false
|
||||
updateValue(event.target.value)
|
||||
}
|
||||
|
||||
const onInput = event => {
|
||||
if (readonly || !updateOnChange) {
|
||||
return
|
||||
}
|
||||
updateValue(event.target.value)
|
||||
}
|
||||
|
||||
const updateValueOnEnter = event => {
|
||||
if (readonly) {
|
||||
return
|
||||
}
|
||||
if (event.key === "Enter") {
|
||||
updateValue(event.target.value)
|
||||
}
|
||||
}
|
||||
|
||||
const getFilteredOptions = (options, term, getLabel) => {
|
||||
if (autocomplete && term) {
|
||||
const lowerCaseTerm = term.toLowerCase()
|
||||
return options.filter(option => {
|
||||
return `${getLabel(option)}`.toLowerCase().includes(lowerCaseTerm)
|
||||
})
|
||||
}
|
||||
return options
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
@ -155,11 +125,12 @@
|
|||
value={primaryLabel || ""}
|
||||
placeholder={placeholder || ""}
|
||||
{disabled}
|
||||
{readonly}
|
||||
readonly
|
||||
class="spectrum-Textfield-input spectrum-InputGroup-input"
|
||||
class:labelPadding={iconData}
|
||||
class:open={primaryOpen}
|
||||
/>
|
||||
{#if primaryValue}
|
||||
{#if primaryValue && showClearIcon}
|
||||
<button
|
||||
on:click={() => onClearPrimary()}
|
||||
type="reset"
|
||||
|
@ -183,6 +154,15 @@
|
|||
class:auto-width={autoWidth}
|
||||
class:is-full-width={!secondaryOptions.length}
|
||||
>
|
||||
{#if autocomplete}
|
||||
<Search
|
||||
value={searchTerm}
|
||||
on:change={event => updateSearch(event)}
|
||||
{disabled}
|
||||
placeholder="Search"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<ul class="spectrum-Menu" role="listbox">
|
||||
{#if placeholderOption}
|
||||
<li
|
||||
|
@ -204,7 +184,7 @@
|
|||
</li>
|
||||
{/if}
|
||||
{#each groupTitles as title}
|
||||
<div class="spectrum-Menu-item">
|
||||
<div class="spectrum-Menu-item title">
|
||||
<Detail>{title}</Detail>
|
||||
</div>
|
||||
{#if primaryOptions}
|
||||
|
@ -239,7 +219,10 @@
|
|||
</div>
|
||||
{:else if getPrimaryOptionColour(option, idx)}
|
||||
<span class="option-left">
|
||||
<StatusLight color={getPrimaryOptionColour(option, idx)} />
|
||||
<StatusLight
|
||||
square
|
||||
color={getPrimaryOptionColour(option, idx)}
|
||||
/>
|
||||
</span>
|
||||
{/if}
|
||||
<span class="spectrum-Menu-itemLabel">
|
||||
|
@ -259,6 +242,7 @@
|
|||
{#if getPrimaryOptionIcon(option, idx) && getPrimaryOptionColour(option, idx)}
|
||||
<span class="option-right">
|
||||
<StatusLight
|
||||
square
|
||||
color={getPrimaryOptionColour(option, idx)}
|
||||
/>
|
||||
</span>
|
||||
|
@ -287,7 +271,7 @@
|
|||
</span>
|
||||
{:else if secondaryFieldColour}
|
||||
<span class="option-left">
|
||||
<StatusLight color={secondaryFieldColour} />
|
||||
<StatusLight square color={secondaryFieldColour} />
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
|
@ -325,6 +309,7 @@
|
|||
{#if getSecondaryOptionColour(option, idx)}
|
||||
<span class="option-left">
|
||||
<StatusLight
|
||||
square
|
||||
color={getSecondaryOptionColour(option, idx)}
|
||||
/>
|
||||
</span>
|
||||
|
@ -357,6 +342,13 @@
|
|||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.spectrum-InputGroup :global(.spectrum-Search-input) {
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--spectrum-global-color-gray-300);
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.override-borders {
|
||||
border-top-left-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
|
@ -427,4 +419,18 @@
|
|||
.spectrum-Search-clearButton {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* Fix focus borders to show only when opened */
|
||||
.spectrum-Textfield-input {
|
||||
border-color: var(--spectrum-global-color-gray-400) !important;
|
||||
border-right-width: 1px;
|
||||
}
|
||||
.spectrum-Textfield-input.open {
|
||||
border-color: var(--spectrum-global-color-blue-400) !important;
|
||||
}
|
||||
|
||||
/* Fix being able to hover and select titles */
|
||||
.spectrum-Menu-item.title {
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
export let error = null
|
||||
export let enableTime = true
|
||||
export let timeOnly = false
|
||||
export let time24hr = false
|
||||
export let placeholder = null
|
||||
export let appendTo = undefined
|
||||
export let ignoreTimezones = false
|
||||
|
@ -30,6 +31,7 @@
|
|||
{placeholder}
|
||||
{enableTime}
|
||||
{timeOnly}
|
||||
{time24hr}
|
||||
{appendTo}
|
||||
{ignoreTimezones}
|
||||
on:change={onChange}
|
||||
|
|
|
@ -26,6 +26,8 @@
|
|||
export let autofocus
|
||||
export let primaryOptions = []
|
||||
export let secondaryOptions = []
|
||||
export let searchTerm
|
||||
export let showClearIcon = true
|
||||
|
||||
let primaryLabel
|
||||
let secondaryLabel
|
||||
|
@ -87,10 +89,15 @@
|
|||
}
|
||||
return value
|
||||
}
|
||||
|
||||
const updateSearchTerm = e => {
|
||||
searchTerm = e.detail
|
||||
}
|
||||
</script>
|
||||
|
||||
<Field {label} {labelPosition} {error}>
|
||||
<PickerDropdown
|
||||
{searchTerm}
|
||||
{autocomplete}
|
||||
{dataCy}
|
||||
{updateOnChange}
|
||||
|
@ -114,8 +121,10 @@
|
|||
{secondaryValue}
|
||||
{primaryLabel}
|
||||
{secondaryLabel}
|
||||
{showClearIcon}
|
||||
on:pickprimary={onPickPrimary}
|
||||
on:picksecondary={onPickSecondary}
|
||||
on:search={updateSearchTerm}
|
||||
on:click
|
||||
on:input
|
||||
on:blur
|
||||
|
|
|
@ -9,11 +9,12 @@
|
|||
export let avatar = false
|
||||
export let title = null
|
||||
export let subtitle = null
|
||||
export let hoverable = false
|
||||
|
||||
$: initials = avatar ? title?.[0] : null
|
||||
</script>
|
||||
|
||||
<div class="list-item">
|
||||
<div class="list-item" class:hoverable on:click>
|
||||
<div class="left">
|
||||
{#if icon}
|
||||
<div class="icon" style="background: {iconBackground || `transparent`};">
|
||||
|
@ -39,11 +40,12 @@
|
|||
.list-item {
|
||||
padding: 0 16px;
|
||||
height: 56px;
|
||||
background: var(--spectrum-alias-background-color-tertiary);
|
||||
background: var(--spectrum-global-color-gray-50);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||
transition: background 130ms ease-out;
|
||||
}
|
||||
.list-item:not(:first-child) {
|
||||
border-top: none;
|
||||
|
@ -56,6 +58,10 @@
|
|||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
.hoverable:hover {
|
||||
cursor: pointer;
|
||||
background: var(--spectrum-global-color-gray-75);
|
||||
}
|
||||
.left,
|
||||
.right {
|
||||
display: flex;
|
||||
|
|
|
@ -106,7 +106,9 @@
|
|||
{/if}
|
||||
|
||||
{#if showCancelButton}
|
||||
<Button group secondary on:click={close}>{cancelText}</Button>
|
||||
<Button group secondary newStyles on:click={close}>
|
||||
{cancelText}
|
||||
</Button>
|
||||
{/if}
|
||||
{#if showConfirmButton}
|
||||
<span class="confirm-wrap">
|
||||
|
|
|
@ -63,7 +63,7 @@
|
|||
|
||||
<style>
|
||||
.spectrum-Popover {
|
||||
min-width: var(--spectrum-global-dimension-size-2000) !important;
|
||||
min-width: var(--spectrum-global-dimension-size-2000);
|
||||
}
|
||||
.spectrum-Popover.is-open.spectrum-Popover--withTip {
|
||||
margin-top: var(--spacing-xs);
|
||||
|
|
|
@ -503,12 +503,6 @@
|
|||
.spectrum-Table-headCell--alignRight {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.spectrum-Table-headCell--divider {
|
||||
padding-right: var(--cell-padding);
|
||||
}
|
||||
.spectrum-Table-headCell--divider + .spectrum-Table-headCell {
|
||||
padding-left: var(--cell-padding);
|
||||
}
|
||||
.spectrum-Table-headCell--edit {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
|
@ -580,13 +574,6 @@
|
|||
background-color: var(--table-bg);
|
||||
z-index: auto;
|
||||
}
|
||||
.spectrum-Table-cell--divider {
|
||||
padding-right: var(--cell-padding);
|
||||
}
|
||||
.spectrum-Table-cell--divider + .spectrum-Table-cell {
|
||||
padding-left: var(--cell-padding);
|
||||
}
|
||||
|
||||
.spectrum-Table-cell--edit {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
|
|
|
@ -19,9 +19,14 @@ filterTests(["smoke", "all"], () => {
|
|||
cy.wait(500)
|
||||
|
||||
// Reset password
|
||||
cy.get(".spectrum-ActionButton-label", { timeout: 2000 }).contains("Force password reset").click({ force: true })
|
||||
cy.get(".title").within(() => {
|
||||
cy.get(interact.SPECTRUM_ICON).click({ force: true })
|
||||
})
|
||||
cy.get(interact.SPECTRUM_MENU).within(() => {
|
||||
cy.get(interact.SPECTRUM_MENU_ITEM).contains("Force password reset").click({ force: true })
|
||||
})
|
||||
|
||||
cy.get(".spectrum-Dialog-grid")
|
||||
cy.get(interact.SPECTRUM_DIALOG_GRID)
|
||||
.find(interact.SPECTRUM_TEXTFIELD_INPUT).invoke('val').as('pwd')
|
||||
|
||||
cy.get(interact.SPECTRUM_BUTTON).contains("Reset password").click({ force: true })
|
||||
|
@ -36,26 +41,32 @@ filterTests(["smoke", "all"], () => {
|
|||
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).eq(i).type("test")
|
||||
}
|
||||
cy.get(interact.SPECTRUM_BUTTON).contains("Reset your password").click({ force: true })
|
||||
//cy.logoutNoAppGrid()
|
||||
})
|
||||
|
||||
it("should verify Standard Portal", () => {
|
||||
// Development access should be disabled (Admin access is already disabled)
|
||||
cy.login()
|
||||
cy.setUserRole("bbuser", "App User")
|
||||
bbUserLogin()
|
||||
|
||||
// Verify Standard Portal
|
||||
cy.get(interact.SPECTRUM_SIDENAV).should('not.exist') // No config sections
|
||||
cy.get(interact.CREATE_APP_BUTTON).should('not.exist') // No create app button
|
||||
cy.get(".app").should('not.exist') // No apps -> no roles assigned to user
|
||||
cy.get(interact.CONTAINER).should('contain', bbUserEmail) // Message containing users email
|
||||
|
||||
cy.logoutNoAppGrid()
|
||||
})
|
||||
|
||||
it("should verify Admin Portal", () => {
|
||||
cy.login()
|
||||
cy.contains("Users").click()
|
||||
cy.contains("bbuser").click()
|
||||
|
||||
// Enable Development & Administration access
|
||||
cy.wait(500)
|
||||
for (let i = 4; i < 6; i++) {
|
||||
cy.get(interact.FIELD).eq(i).within(() => {
|
||||
cy.get(interact.SPECTRUM_SWITCH_INPUT).click({ force: true })
|
||||
cy.get(interact.SPECTRUM_SWITCH_INPUT).should('be.enabled')
|
||||
})
|
||||
}
|
||||
// Configure user role
|
||||
cy.setUserRole("bbuser", "Admin")
|
||||
bbUserLogin()
|
||||
|
||||
// Verify available options for Admin portal
|
||||
cy.get(".spectrum-SideNav")
|
||||
cy.get(interact.SPECTRUM_SIDENAV)
|
||||
.should('contain', 'Apps')
|
||||
//.and('contain', 'Usage')
|
||||
.and('contain', 'Users')
|
||||
|
@ -72,13 +83,7 @@ filterTests(["smoke", "all"], () => {
|
|||
it("should verify Development Portal", () => {
|
||||
// Only Development access should be enabled
|
||||
cy.login()
|
||||
cy.contains("Users").click()
|
||||
cy.contains("bbuser").click()
|
||||
cy.wait(500)
|
||||
cy.get(interact.FIELD).eq(5).within(() => {
|
||||
cy.get(interact.SPECTRUM_SWITCH_INPUT).click({ force: true })
|
||||
})
|
||||
|
||||
cy.setUserRole("bbuser", "Developer")
|
||||
bbUserLogin()
|
||||
|
||||
// Verify available options for Admin portal
|
||||
|
@ -96,27 +101,6 @@ filterTests(["smoke", "all"], () => {
|
|||
cy.logOut()
|
||||
})
|
||||
|
||||
it("should verify Standard Portal", () => {
|
||||
// Development access should be disabled (Admin access is already disabled)
|
||||
cy.login()
|
||||
cy.contains("Users").click()
|
||||
cy.contains("bbuser").click()
|
||||
cy.wait(500)
|
||||
cy.get(interact.FIELD).eq(4).within(() => {
|
||||
cy.get(interact.SPECTRUM_SWITCH_INPUT).click({ force: true })
|
||||
})
|
||||
|
||||
bbUserLogin()
|
||||
|
||||
// Verify Standard Portal
|
||||
cy.get(interact.SPECTRUM_SIDENAV).should('not.exist') // No config sections
|
||||
cy.get(interact.CREATE_APP_BUTTON).should('not.exist') // No create app button
|
||||
cy.get(".app").should('not.exist') // No apps -> no roles assigned to user
|
||||
cy.get(interact.CONTAINER).should('contain', bbUserEmail) // Message containing users email
|
||||
|
||||
cy.logoutNoAppGrid()
|
||||
})
|
||||
|
||||
const bbUserLogin = () => {
|
||||
// Login as bbuser
|
||||
cy.logOut()
|
||||
|
|
|
@ -15,25 +15,16 @@ filterTests(["smoke", "all"], () => {
|
|||
cy.get(interact.SPECTRUM_TABLE).should("contain", "bbuser")
|
||||
})
|
||||
|
||||
it("should confirm basic permission for a New User", () => {
|
||||
// Basic permission = development & administraton disabled
|
||||
it("should confirm App User role for a New User", () => {
|
||||
cy.contains("bbuser").click()
|
||||
// Confirm development and admin access are disabled
|
||||
for (let i = 4; i < 6; i++) {
|
||||
cy.wait(500)
|
||||
cy.get(interact.FIELD).eq(i).within(() => {
|
||||
//cy.get(interact.SPECTRUM_SWITCH_INPUT).should('be.disabled')
|
||||
cy.get(".spectrum-Switch-switch").should('not.be.checked')
|
||||
})
|
||||
}
|
||||
// Existing apps appear within the No Access table
|
||||
cy.get(interact.SPECTRUM_TABLE, { timeout: 500 }).eq(1).should("not.contain", "No rows found")
|
||||
// Configure roles table should not contain apps
|
||||
cy.get(interact.SPECTRUM_TABLE).eq(0).contains("No rows found")
|
||||
cy.get(".spectrum-Form-itemField").eq(3).should('contain', 'App User')
|
||||
|
||||
// User should not have app access
|
||||
cy.get(interact.LIST_ITEMS, { timeout: 500 }).should("contain", "No apps")
|
||||
})
|
||||
|
||||
if (Cypress.env("TEST_ENV")) {
|
||||
it("should assign role types", () => {
|
||||
xit("should assign role types", () => {
|
||||
// 3 apps minimum required - to assign an app to each role type
|
||||
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
||||
.its("body")
|
||||
|
@ -96,7 +87,7 @@ filterTests(["smoke", "all"], () => {
|
|||
})
|
||||
})
|
||||
|
||||
it("should unassign role types", () => {
|
||||
xit("should unassign role types", () => {
|
||||
// Set each app within Configure roles table to 'No Access'
|
||||
cy.get(interact.SPECTRUM_TABLE)
|
||||
.eq(0)
|
||||
|
@ -125,7 +116,7 @@ filterTests(["smoke", "all"], () => {
|
|||
})
|
||||
}
|
||||
|
||||
it("should enable Developer access and verify application access", () => {
|
||||
xit("should enable Developer access and verify application access", () => {
|
||||
// Enable Developer access
|
||||
cy.get(interact.FIELD)
|
||||
.eq(4)
|
||||
|
@ -157,7 +148,7 @@ filterTests(["smoke", "all"], () => {
|
|||
})
|
||||
})
|
||||
|
||||
it("should disable Developer access and verify application access", () => {
|
||||
xit("should disable Developer access and verify application access", () => {
|
||||
// Disable Developer access
|
||||
cy.get(interact.FIELD)
|
||||
.eq(4)
|
||||
|
@ -175,12 +166,12 @@ filterTests(["smoke", "all"], () => {
|
|||
|
||||
it("Should edit user details within user details page", () => {
|
||||
// Add First name
|
||||
cy.get(interact.FIELD, { timeout: 1000 }).eq(2).within(() => {
|
||||
cy.get(interact.FIELD, { timeout: 1000 }).eq(1).within(() => {
|
||||
cy.wait(500)
|
||||
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).wait(500).clear().click().type("bb")
|
||||
})
|
||||
// Add Last name
|
||||
cy.get(interact.FIELD, { timeout: 1000 }).eq(3).within(() => {
|
||||
cy.get(interact.FIELD, { timeout: 1000 }).eq(2).within(() => {
|
||||
cy.wait(500)
|
||||
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).click().wait(500).clear().type("test")
|
||||
})
|
||||
|
@ -189,21 +180,27 @@ filterTests(["smoke", "all"], () => {
|
|||
cy.reload()
|
||||
|
||||
// Confirm details have been saved
|
||||
cy.get(interact.FIELD, { timeout: 1000 }).eq(2).within(() => {
|
||||
cy.get(interact.FIELD, { timeout: 1000 }).eq(1).within(() => {
|
||||
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', "bb")
|
||||
})
|
||||
cy.get(interact.FIELD, { timeout: 1000 }).eq(3).within(() => {
|
||||
cy.get(interact.FIELD, { timeout: 1000 }).eq(2).within(() => {
|
||||
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).should('have.value', "test")
|
||||
})
|
||||
})
|
||||
|
||||
it("should reset the users password", () => {
|
||||
cy.get(interact.REGENERATE, { timeout: 500 }).contains("Force password reset").click({ force: true })
|
||||
cy.get(".title").within(() => {
|
||||
cy.get(interact.SPECTRUM_ICON).click({ force: true })
|
||||
})
|
||||
cy.get(interact.SPECTRUM_MENU).within(() => {
|
||||
cy.get(interact.SPECTRUM_MENU_ITEM).contains("Force password reset").click({ force: true })
|
||||
})
|
||||
|
||||
// Reset password modal
|
||||
cy.get(interact.SPECTRUM_DIALOG_GRID)
|
||||
.find(interact.SPECTRUM_TEXTFIELD_INPUT).invoke('val').as('pwd')
|
||||
cy.get(interact.SPECTRUM_BUTTON).contains("Reset password").click({ force: true })
|
||||
cy.get(interact.SPECTRUM_BUTTON).contains("Reset password").should('not.exist')
|
||||
|
||||
// Logout, then login with new password
|
||||
cy.logOut()
|
||||
|
@ -218,6 +215,7 @@ filterTests(["smoke", "all"], () => {
|
|||
cy.get(interact.SPECTRUM_BUTTON).contains("Reset your password").click({ force: true })
|
||||
|
||||
// Confirm user logged in afer password change
|
||||
cy.login("bbuser@test.com", "test")
|
||||
cy.get(".avatar > .icon").click({ force: true })
|
||||
|
||||
cy.get(".spectrum-Menu-item").contains("Update user information").click({ force: true })
|
||||
|
|
|
@ -19,10 +19,10 @@ filterTests(["smoke", "all"], () => {
|
|||
cy.contains("Users").click()
|
||||
cy.contains("test@test.com").click()
|
||||
|
||||
cy.get(interact.FIELD, { timeout: 1000 }).eq(2).within(() => {
|
||||
cy.get(interact.FIELD, { timeout: 1000 }).eq(1).within(() => {
|
||||
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', fname)
|
||||
})
|
||||
cy.get(interact.FIELD).eq(3).within(() => {
|
||||
cy.get(interact.FIELD).eq(2).within(() => {
|
||||
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', lname)
|
||||
})
|
||||
})
|
||||
|
@ -72,7 +72,7 @@ filterTests(["smoke", "all"], () => {
|
|||
})
|
||||
|
||||
// Logout & in with new password
|
||||
cy.logOut()
|
||||
//cy.logOut()
|
||||
cy.login("test@test.com", "newpwd")
|
||||
})
|
||||
|
||||
|
@ -90,7 +90,6 @@ filterTests(["smoke", "all"], () => {
|
|||
cy.get(interact.SPECTRUM_MENU_ITEM).contains("Open developer mode").click({ force: true })
|
||||
cy.get(interact.SPECTRUM_SIDENAV).should('exist') // config sections available
|
||||
cy.get(interact.CREATE_APP_BUTTON).should('exist') // create app button available
|
||||
cy.get(interact.APP_TABLE).should('exist') // App table available
|
||||
})
|
||||
|
||||
after(() => {
|
||||
|
|
|
@ -266,7 +266,7 @@ filterTests(["all"], () => {
|
|||
cy.reload()
|
||||
cy.log("Current deployment version: " + clientPackage.version)
|
||||
|
||||
cy.get(".version-status a", { timeout: 1000 }).contains("Update").click()
|
||||
cy.get(".version-status a", { timeout: 5000 }).contains("Update").click()
|
||||
cy.get(".spectrum-Tabs-item.is-selected").contains("Settings")
|
||||
|
||||
cy.get(".version-section .page-action button")
|
||||
|
|
|
@ -10,10 +10,8 @@ filterTests(['smoke', 'all'], () => {
|
|||
|
||||
it("should disable the autogenerated screen options if no sources are available", () => {
|
||||
cy.createApp("First Test App", false)
|
||||
|
||||
cy.closeModal();
|
||||
|
||||
cy.contains("Design").click()
|
||||
cy.navigateToAutogeneratedModal()
|
||||
cy.get(interact.CONFIRM_WRAP_SPE_BUTTON).should('be.disabled')
|
||||
|
||||
|
|
|
@ -94,6 +94,7 @@ filterTests(['smoke', 'all'], () => {
|
|||
})
|
||||
|
||||
it("should create the first application from scratch with a default name", () => {
|
||||
cy.updateUserInformation("", "")
|
||||
cy.createApp("", false)
|
||||
cy.applicationInAppTable("My app")
|
||||
cy.deleteApp("My app")
|
||||
|
|
|
@ -48,7 +48,7 @@ filterTests(["smoke", "all"], () => {
|
|||
|
||||
it("deletes a row", () => {
|
||||
cy.get(interact.SPECTRUM_CHECKBOX_INPUT).check({ force: true })
|
||||
cy.contains("Delete 1 row(s)").click()
|
||||
cy.contains("Delete 1 row").click()
|
||||
cy.get(interact.SPECTRUM_MODAL).contains("Delete").click()
|
||||
cy.contains("RoverUpdated").should("not.exist")
|
||||
})
|
||||
|
|
|
@ -199,15 +199,16 @@ filterTests(["all"], () => {
|
|||
.within(() => {
|
||||
cy.get("input").clear().type(queryRename)
|
||||
})
|
||||
// Save query
|
||||
cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
|
||||
// Click on a nav item
|
||||
cy.get(".nav-item").first().click()
|
||||
// Confirm name change
|
||||
cy.get(".nav-item").should("contain", queryRename)
|
||||
})
|
||||
|
||||
it("should delete a query", () => {
|
||||
// Get query nav item - QueryName
|
||||
cy.get(".nav-item")
|
||||
.contains(queryName)
|
||||
.contains(queryRename)
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.get(".spectrum-Icon").eq(1).click({ force: true })
|
||||
|
@ -218,7 +219,7 @@ filterTests(["all"], () => {
|
|||
.contains("Delete Query")
|
||||
.click({ force: true })
|
||||
// Confirm deletion
|
||||
cy.get(".nav-item", { timeout: 1000 }).should("not.contain", queryName)
|
||||
cy.get(".nav-item", { timeout: 1000 }).should("not.contain", queryRename)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
@ -220,7 +220,8 @@ filterTests(["all"], () => {
|
|||
it("should edit a query name", () => {
|
||||
// Access query
|
||||
cy.get(".hierarchy-items-container", { timeout: 2000 })
|
||||
.contains(queryName + " (1)")
|
||||
//.contains(queryName + " (1)")
|
||||
.contains(queryName)
|
||||
.click({ force: true })
|
||||
|
||||
// Rename query
|
||||
|
@ -231,18 +232,16 @@ filterTests(["all"], () => {
|
|||
cy.get("input").clear().type(queryRename)
|
||||
})
|
||||
|
||||
// Run and Save query
|
||||
cy.get(".spectrum-Button", { timeout: 2000 }).contains("Run Query").click({ force: true })
|
||||
cy.wait(1000)
|
||||
cy.get(".spectrum-Button", { timeout: 2000 }).contains("Save Query").click({ force: true })
|
||||
cy.reload({ timeout: 5000 })
|
||||
cy.get(".nav-item", { timeout: 2000 }).should("contain", queryRename)
|
||||
// Click on a nav item and confirm name change
|
||||
cy.get(".nav-item").first().click()
|
||||
// Confirm name change
|
||||
cy.get(".nav-item").should("contain", queryRename)
|
||||
})
|
||||
|
||||
it("should delete a query", () => {
|
||||
// Get query nav item - QueryName
|
||||
cy.get(".nav-item")
|
||||
.contains(queryName)
|
||||
.contains(queryRename)
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.get(".spectrum-Icon").eq(1).click({ force: true })
|
||||
|
@ -254,7 +253,7 @@ filterTests(["all"], () => {
|
|||
.click({ force: true })
|
||||
// Confirm deletion
|
||||
cy.reload({ timeout: 5000 })
|
||||
cy.get(".nav-item", { timeout: 1000 }).should("not.contain", queryName)
|
||||
cy.get(".nav-item", { timeout: 1000 }).should("not.contain", queryRename)
|
||||
})
|
||||
|
||||
const switchSchema = schema => {
|
||||
|
|
|
@ -5,30 +5,31 @@ Cypress.on("uncaught:exception", () => {
|
|||
// ACCOUNTS & USERS
|
||||
Cypress.Commands.add("login", (email, password) => {
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 10000 })
|
||||
cy.wait(2000)
|
||||
cy.url().then(url => {
|
||||
if (url.includes("builder/admin")) {
|
||||
// create admin user
|
||||
cy.get("input").first().type("test@test.com")
|
||||
cy.get('input[type="password"]').first().type("test")
|
||||
cy.get('input[type="password"]').eq(1).type("test")
|
||||
cy.contains("Create super admin user").click({ force: true })
|
||||
}
|
||||
if (url.includes("builder/auth/login") || url.includes("builder/admin")) {
|
||||
// login
|
||||
cy.contains("Sign in to Budibase").then(() => {
|
||||
if (email == null) {
|
||||
cy.get("input").first().type("test@test.com")
|
||||
cy.get('input[type="password"]').type("test")
|
||||
} else {
|
||||
cy.get("input").first().type(email)
|
||||
cy.get('input[type="password"]').type(password)
|
||||
}
|
||||
cy.get("button").first().click({ force: true })
|
||||
cy.wait(1000)
|
||||
})
|
||||
}
|
||||
})
|
||||
cy.url()
|
||||
.should("include", "/builder/")
|
||||
.then(url => {
|
||||
if (url.includes("builder/admin")) {
|
||||
// create admin user
|
||||
cy.get("input").first().type("test@test.com")
|
||||
cy.get('input[type="password"]').first().type("test")
|
||||
cy.get('input[type="password"]').eq(1).type("test")
|
||||
cy.contains("Create super admin user").click({ force: true })
|
||||
}
|
||||
if (url.includes("builder/auth") || url.includes("builder/admin")) {
|
||||
// login
|
||||
cy.contains("Sign in to Budibase").then(() => {
|
||||
if (email == null) {
|
||||
cy.get("input").first().type("test@test.com")
|
||||
cy.get('input[type="password"]').type("test")
|
||||
} else {
|
||||
cy.get("input").first().type(email)
|
||||
cy.get('input[type="password"]').type(password)
|
||||
}
|
||||
cy.get("button").first().click({ force: true })
|
||||
cy.wait(1000)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Cypress.Commands.add("logOut", () => {
|
||||
|
@ -50,23 +51,36 @@ Cypress.Commands.add("logoutNoAppGrid", () => {
|
|||
cy.wait(2000)
|
||||
})
|
||||
|
||||
Cypress.Commands.add("createUser", email => {
|
||||
// quick hacky recorded way to create a user
|
||||
Cypress.Commands.add("createUser", (email, permission) => {
|
||||
cy.contains("Users").click()
|
||||
cy.get(`[data-cy="add-user"]`).click()
|
||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||
cy.get(".spectrum-Picker-label").click()
|
||||
cy.get(
|
||||
".spectrum-Menu-item:nth-child(2) > .spectrum-Menu-itemLabel"
|
||||
).click()
|
||||
// Enter email
|
||||
cy.get(".spectrum-Textfield-input").clear().click().type(email)
|
||||
|
||||
// Onboarding type selector
|
||||
cy.get(".spectrum-Textfield-input")
|
||||
.eq(0)
|
||||
.first()
|
||||
.type(email, { force: true })
|
||||
cy.get(".spectrum-Button--cta").click({ force: true })
|
||||
// Select permission, if applicable
|
||||
// Default is App User
|
||||
if (permission != null) {
|
||||
cy.get(".spectrum-Picker-label").click()
|
||||
cy.get(".spectrum-Menu").within(() => {
|
||||
cy.get(".spectrum-Menu-item")
|
||||
.contains(permission)
|
||||
.click({ force: true })
|
||||
})
|
||||
}
|
||||
// Add user and wait for modal to change
|
||||
cy.get(".spectrum-Button").contains("Add user").click({ force: true })
|
||||
cy.get(".spectrum-ActionButton").contains("Add email").should("not.exist")
|
||||
})
|
||||
// Onboarding modal
|
||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||
cy.get(".onboarding-type").eq(1).click()
|
||||
cy.get(".spectrum-Button").contains("Done").click({ force: true })
|
||||
cy.get(".spectrum-Button").contains("Cancel").should("not.exist")
|
||||
})
|
||||
|
||||
// Accounts created modal - Click Done button
|
||||
cy.get(".spectrum-Button").contains("Done").click({ force: true })
|
||||
})
|
||||
|
||||
Cypress.Commands.add("deleteUser", email => {
|
||||
|
@ -74,18 +88,13 @@ Cypress.Commands.add("deleteUser", email => {
|
|||
cy.contains("Users", { timeout: 2000 }).click()
|
||||
cy.contains(email).click()
|
||||
|
||||
// Click Delete user button
|
||||
cy.get(".spectrum-Button")
|
||||
.contains("Delete user")
|
||||
.click({ force: true })
|
||||
.then(() => {
|
||||
// Confirm deletion within modal
|
||||
cy.get(".spectrum-Dialog-grid", { timeout: 500 }).within(() => {
|
||||
cy.get(".spectrum-Button")
|
||||
.contains("Delete user")
|
||||
.click({ force: true })
|
||||
})
|
||||
})
|
||||
cy.get(".title").within(() => {
|
||||
cy.get(".spectrum-Icon").click({ force: true })
|
||||
})
|
||||
cy.get(".spectrum-Menu").within(() => {
|
||||
cy.get(".spectrum-Menu-item").contains("Delete").click({ force: true })
|
||||
})
|
||||
cy.get(".spectrum-Dialog-grid").contains("Delete user").click({ force: true })
|
||||
})
|
||||
|
||||
Cypress.Commands.add("updateUserInformation", (firstName, lastName) => {
|
||||
|
@ -119,10 +128,30 @@ Cypress.Commands.add("updateUserInformation", (firstName, lastName) => {
|
|||
.should("have.value", lastName)
|
||||
.blur()
|
||||
}
|
||||
cy.get("button").contains("Update information").click({ force: true })
|
||||
cy.get(".confirm-wrap").within(() => {
|
||||
cy.get("button").contains("Update information").click({ force: true })
|
||||
})
|
||||
cy.get(".spectrum-Dialog-grid").should("not.exist")
|
||||
})
|
||||
})
|
||||
|
||||
Cypress.Commands.add("setUserRole", (user, role) => {
|
||||
cy.contains("Users").click()
|
||||
cy.contains(user).click()
|
||||
|
||||
// Set Role
|
||||
cy.wait(500)
|
||||
cy.get(".spectrum-Form-itemField")
|
||||
.eq(3)
|
||||
.within(() => {
|
||||
cy.get(".spectrum-Picker-label").click({ force: true })
|
||||
})
|
||||
cy.get(".spectrum-Menu").within(() => {
|
||||
cy.get(".spectrum-Menu-itemLabel").contains(role).click({ force: true })
|
||||
})
|
||||
cy.get(".spectrum-Form-itemField").eq(3).should("contain", role)
|
||||
})
|
||||
|
||||
// APPLICATIONS
|
||||
Cypress.Commands.add("createTestApp", () => {
|
||||
const appName = "Cypress Tests"
|
||||
|
@ -135,7 +164,7 @@ Cypress.Commands.add("createApp", (name, addDefaultTable) => {
|
|||
typeof addDefaultTable != "boolean" ? true : addDefaultTable
|
||||
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 10000 })
|
||||
cy.wait(1000)
|
||||
cy.url({ timeout: 30000 }).should("include", "/apps")
|
||||
cy.get(`[data-cy="create-app-btn"]`, { timeout: 5000 }).click({ force: true })
|
||||
|
||||
// If apps already exist
|
||||
|
@ -405,6 +434,7 @@ Cypress.Commands.add("createAppFromScratch", appName => {
|
|||
|
||||
// TABLES
|
||||
Cypress.Commands.add("createTable", (tableName, initialTable) => {
|
||||
// Creates an internal Budibase DB table
|
||||
if (!initialTable) {
|
||||
cy.navigateToDataSection()
|
||||
cy.get(`[data-cy="new-table"]`, { timeout: 2000 }).click()
|
||||
|
@ -418,6 +448,10 @@ Cypress.Commands.add("createTable", (tableName, initialTable) => {
|
|||
.contains("Continue")
|
||||
.click({ force: true })
|
||||
})
|
||||
cy.get(".spectrum-Modal", { timeout: 10000 }).should(
|
||||
"not.contain",
|
||||
"Add data source"
|
||||
)
|
||||
cy.get(".spectrum-Modal", { timeout: 2000 }).within(() => {
|
||||
cy.get("input", { timeout: 2000 }).first().type(tableName).blur()
|
||||
cy.get(".spectrum-ButtonGroup").contains("Create").click()
|
||||
|
@ -516,15 +550,22 @@ Cypress.Commands.add("addCustomSourceOptions", totalOptions => {
|
|||
// DESIGN SECTION
|
||||
Cypress.Commands.add("searchAndAddComponent", component => {
|
||||
// Open component menu
|
||||
cy.get(".spectrum-Button").contains("Component").click({ force: true })
|
||||
cy.get(".icon-side-nav").within(() => {
|
||||
cy.get(".icon-side-nav-item").eq(1).click()
|
||||
})
|
||||
cy.get(".add-component > .spectrum-Button")
|
||||
.contains("Add component")
|
||||
.click({ force: true })
|
||||
cy.get(".container", { timeout: 1000 }).within(() => {
|
||||
cy.get(".title").should("contain", "Add component")
|
||||
|
||||
// Search and add component
|
||||
cy.wait(500)
|
||||
cy.get(".spectrum-Textfield-input").clear().type(component)
|
||||
cy.get(".body").within(() => {
|
||||
cy.get(".component")
|
||||
.contains(new RegExp("^" + component + "$"), { timeout: 3000 })
|
||||
.click({ force: true })
|
||||
// Search and add component
|
||||
cy.get(".spectrum-Textfield-input").clear().type(component)
|
||||
cy.get(".body").within(() => {
|
||||
cy.get(".component")
|
||||
.contains(new RegExp("^" + component + "$"), { timeout: 3000 })
|
||||
.click({ force: true })
|
||||
})
|
||||
})
|
||||
cy.wait(1000)
|
||||
cy.location().then(loc => {
|
||||
|
@ -570,7 +611,7 @@ Cypress.Commands.add("getComponent", componentId => {
|
|||
Cypress.Commands.add("createScreen", (route, accessLevelLabel) => {
|
||||
// Blank Screen
|
||||
cy.contains("Design").click()
|
||||
cy.get(".header > .add-button").click()
|
||||
cy.get(".spectrum-Button").contains("Add screen").click({ force: true })
|
||||
cy.get(".spectrum-Modal").within(() => {
|
||||
cy.get("[data-cy='blank-screen']").click()
|
||||
cy.get(".spectrum-Button").contains("Continue").click({ force: true })
|
||||
|
@ -595,7 +636,7 @@ Cypress.Commands.add(
|
|||
"createDatasourceScreen",
|
||||
(datasourceNames, accessLevelLabel) => {
|
||||
cy.contains("Design").click()
|
||||
cy.get(".header > .add-button").click()
|
||||
cy.get(".spectrum-Button").contains("Add screen").click({ force: true })
|
||||
cy.get(".spectrum-Modal").within(() => {
|
||||
cy.get(".item").contains("Autogenerated screens").click()
|
||||
cy.get(".spectrum-Button").contains("Continue").click({ force: true })
|
||||
|
@ -715,7 +756,7 @@ Cypress.Commands.add("navigateToDataSection", () => {
|
|||
Cypress.Commands.add("navigateToAutogeneratedModal", () => {
|
||||
// Screen name must already exist within data source
|
||||
cy.contains("Design").click()
|
||||
cy.get(".header > .add-button").click()
|
||||
cy.get(".spectrum-Button").contains("Add screen").click({ force: true })
|
||||
cy.get(".spectrum-Modal").within(() => {
|
||||
cy.get(".item", { timeout: 2000 })
|
||||
.contains("Autogenerated screens")
|
||||
|
|
|
@ -109,6 +109,8 @@ export const REGENERATE = ".regenerate"
|
|||
export const SPECTRUM_DIALOG_CONTENT = ".spectrum-Dialog-content"
|
||||
export const SPECTRUM_ICON = ".spectrum-Icon"
|
||||
export const SPECTRUM_HEADING = ".spectrum-Heading"
|
||||
export const SPECTRUM_FORM_ITEMFIELD = ".spectrum-Form-itemField"
|
||||
export const LIST_ITEMS = ".list-items"
|
||||
|
||||
//createView
|
||||
export const SPECTRUM_MENU_ITEM_LABEL = ".spectrum-Menu-itemLabel"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/builder",
|
||||
"version": "1.1.29-alpha.2",
|
||||
"version": "1.2.39-alpha.5",
|
||||
"license": "GPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -69,10 +69,10 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.1.29-alpha.2",
|
||||
"@budibase/client": "^1.1.29-alpha.2",
|
||||
"@budibase/frontend-core": "^1.1.29-alpha.2",
|
||||
"@budibase/string-templates": "^1.1.29-alpha.2",
|
||||
"@budibase/bbui": "1.2.39-alpha.5",
|
||||
"@budibase/client": "1.2.39-alpha.5",
|
||||
"@budibase/frontend-core": "1.2.39-alpha.5",
|
||||
"@budibase/string-templates": "1.2.39-alpha.5",
|
||||
"@sentry/browser": "5.19.1",
|
||||
"@spectrum-css/page": "^3.0.1",
|
||||
"@spectrum-css/vars": "^3.0.1",
|
||||
|
|
|
@ -11,7 +11,7 @@ export default class PosthogClient {
|
|||
|
||||
posthog.init(this.token, {
|
||||
autocapture: false,
|
||||
capture_pageview: true,
|
||||
capture_pageview: false,
|
||||
})
|
||||
posthog.set_config({ persistence: "cookie" })
|
||||
|
||||
|
|
|
@ -32,7 +32,8 @@
|
|||
if (!results) {
|
||||
return {}
|
||||
}
|
||||
if (results.outputs?.status?.toLowerCase() === "stopped") {
|
||||
const lcStatus = results.outputs?.status?.toLowerCase()
|
||||
if (lcStatus === "stopped" || lcStatus === "stopped_error") {
|
||||
return { yellow: true, message: "Stopped" }
|
||||
} else if (results.outputs?.success || isTrigger) {
|
||||
return { positive: true, message: "Success" }
|
||||
|
|
|
@ -79,7 +79,7 @@
|
|||
automationStore.actions.addTestDataToAutomation({
|
||||
body: {
|
||||
[key]: e.detail,
|
||||
...$automationStore.selectedAutomation.automation.testData.body,
|
||||
...$automationStore.selectedAutomation.automation.testData?.body,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -14,7 +14,13 @@
|
|||
import Table from "./Table.svelte"
|
||||
import { TableNames } from "constants"
|
||||
import CreateEditRow from "./modals/CreateEditRow.svelte"
|
||||
import { Pagination, Heading, Body, Layout } from "@budibase/bbui"
|
||||
import {
|
||||
Pagination,
|
||||
Heading,
|
||||
Body,
|
||||
Layout,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { fetchData } from "@budibase/frontend-core"
|
||||
import { API } from "api"
|
||||
|
||||
|
@ -29,6 +35,13 @@
|
|||
$: fetch = createFetch(id)
|
||||
$: hasCols = checkHasCols(schema)
|
||||
$: hasRows = !!$fetch.rows?.length
|
||||
$: showError($fetch.error)
|
||||
|
||||
const showError = error => {
|
||||
if (error) {
|
||||
notifications.error(error?.message || "Unable to fetch data.")
|
||||
}
|
||||
}
|
||||
|
||||
const enrichSchema = schema => {
|
||||
let tempSchema = { ...schema }
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
export let selectedRows
|
||||
export let deleteRows
|
||||
export let item = "row"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let modal
|
||||
|
@ -14,12 +15,14 @@
|
|||
modal?.hide()
|
||||
dispatch("updaterows")
|
||||
}
|
||||
|
||||
$: text = `${item}${selectedRows?.length === 1 ? "" : "s"}`
|
||||
</script>
|
||||
|
||||
<Button icon="Delete" size="s" primary quiet on:click={modal.show}>
|
||||
Delete
|
||||
{selectedRows.length}
|
||||
row(s)
|
||||
{text}
|
||||
</Button>
|
||||
<ConfirmDialog
|
||||
bind:this={modal}
|
||||
|
@ -29,5 +32,5 @@
|
|||
>
|
||||
Are you sure you want to delete
|
||||
{selectedRows.length}
|
||||
row{selectedRows.length > 1 ? "s" : ""}?
|
||||
{text}?
|
||||
</ConfirmDialog>
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
Modal,
|
||||
notifications,
|
||||
ProgressCircle,
|
||||
Layout,
|
||||
Body,
|
||||
} from "@budibase/bbui"
|
||||
import { auth, apps } from "stores/portal"
|
||||
import { processStringSync } from "@budibase/string-templates"
|
||||
|
@ -72,62 +74,67 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
<Modal bind:this={appLockModal}>
|
||||
<ModalContent
|
||||
title={lockedByHeading}
|
||||
dataCy={"app-lock-modal"}
|
||||
showConfirmButton={false}
|
||||
showCancelButton={false}
|
||||
>
|
||||
<p>
|
||||
Apps are locked to prevent work from being lost from overlapping changes
|
||||
between your team.
|
||||
</p>
|
||||
|
||||
{#if lockedByYou && getExpiryDuration(app) > 0}
|
||||
<span class="lock-expiry-body">
|
||||
{processStringSync(
|
||||
"This lock will expire in {{ duration time 'millisecond' }} from now.",
|
||||
{
|
||||
time: getExpiryDuration(app),
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
{/if}
|
||||
<div class="lock-modal-actions">
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
secondary
|
||||
quiet={lockedBy && lockedByYou}
|
||||
disabled={processing}
|
||||
on:click={() => {
|
||||
appLockModal.hide()
|
||||
}}
|
||||
>
|
||||
<span class="cancel"
|
||||
>{lockedBy && !lockedByYou ? "Done" : "Cancel"}</span
|
||||
>
|
||||
</Button>
|
||||
{#if lockedByYou}
|
||||
<Button
|
||||
secondary
|
||||
disabled={processing}
|
||||
on:click={() => {
|
||||
releaseLock()
|
||||
appLockModal.hide()
|
||||
}}
|
||||
>
|
||||
{#if processing}
|
||||
<ProgressCircle overBackground={true} size="S" />
|
||||
{:else}
|
||||
<span class="unlock">Release Lock</span>
|
||||
{/if}
|
||||
</Button>
|
||||
{/if}
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
{#key app}
|
||||
<div>
|
||||
<Modal bind:this={appLockModal}>
|
||||
<ModalContent
|
||||
title={lockedByHeading}
|
||||
dataCy={"app-lock-modal"}
|
||||
showConfirmButton={false}
|
||||
showCancelButton={false}
|
||||
>
|
||||
<Layout noPadding>
|
||||
<Body size="S">
|
||||
Apps are locked to prevent work from being lost from overlapping
|
||||
changes between your team.
|
||||
</Body>
|
||||
{#if lockedByYou && getExpiryDuration(app) > 0}
|
||||
<span class="lock-expiry-body">
|
||||
{processStringSync(
|
||||
"This lock will expire in {{ duration time 'millisecond' }} from now. This lock will expire in This lock will expire in ",
|
||||
{
|
||||
time: getExpiryDuration(app),
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
{/if}
|
||||
<div class="lock-modal-actions">
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
secondary
|
||||
quiet={lockedBy && lockedByYou}
|
||||
disabled={processing}
|
||||
on:click={() => {
|
||||
appLockModal.hide()
|
||||
}}
|
||||
>
|
||||
<span class="cancel"
|
||||
>{lockedBy && !lockedByYou ? "Done" : "Cancel"}</span
|
||||
>
|
||||
</Button>
|
||||
{#if lockedByYou}
|
||||
<Button
|
||||
secondary
|
||||
disabled={processing}
|
||||
on:click={() => {
|
||||
releaseLock()
|
||||
appLockModal.hide()
|
||||
}}
|
||||
>
|
||||
{#if processing}
|
||||
<ProgressCircle overBackground={true} size="S" />
|
||||
{:else}
|
||||
<span class="unlock">Release Lock</span>
|
||||
{/if}
|
||||
</Button>
|
||||
{/if}
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</Layout>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</div>
|
||||
{/key}
|
||||
|
||||
<style>
|
||||
.lock-modal-actions {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
Tab,
|
||||
Body,
|
||||
Layout,
|
||||
Button,
|
||||
} from "@budibase/bbui"
|
||||
import { createEventDispatcher, onMount } from "svelte"
|
||||
import {
|
||||
|
@ -15,10 +16,15 @@
|
|||
decodeJSBinding,
|
||||
encodeJSBinding,
|
||||
} from "@budibase/string-templates"
|
||||
import { readableToRuntimeBinding } from "builderStore/dataBinding"
|
||||
import {
|
||||
readableToRuntimeBinding,
|
||||
runtimeToReadableBinding,
|
||||
} from "builderStore/dataBinding"
|
||||
import { handlebarsCompletions } from "constants/completions"
|
||||
import { addHBSBinding, addJSBinding } from "./utils"
|
||||
import CodeMirrorEditor from "components/common/CodeMirrorEditor.svelte"
|
||||
import { convertToJS } from "@budibase/string-templates"
|
||||
import { admin } from "stores/portal"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -62,15 +68,24 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Adds a HBS helper to the expression
|
||||
const addHelper = helper => {
|
||||
hbsValue = addHBSBinding(hbsValue, getCaretPosition(), helper.text)
|
||||
updateValue(hbsValue)
|
||||
// Adds a JS/HBS helper to the expression
|
||||
const addHelper = (helper, js) => {
|
||||
let tempVal
|
||||
const pos = getCaretPosition()
|
||||
if (js) {
|
||||
const decoded = decodeJSBinding(jsValue)
|
||||
tempVal = jsValue = encodeJSBinding(
|
||||
addJSBinding(decoded, pos, helper.text, { helper: true })
|
||||
)
|
||||
} else {
|
||||
tempVal = hbsValue = addHBSBinding(hbsValue, pos, helper.text)
|
||||
}
|
||||
updateValue(tempVal)
|
||||
}
|
||||
|
||||
// Adds a data binding to the expression
|
||||
const addBinding = binding => {
|
||||
if (usingJS) {
|
||||
const addBinding = (binding, { forceJS } = {}) => {
|
||||
if (usingJS || forceJS) {
|
||||
let js = decodeJSBinding(jsValue)
|
||||
js = addJSBinding(js, getCaretPosition(), binding.readableBinding)
|
||||
jsValue = encodeJSBinding(js)
|
||||
|
@ -100,6 +115,26 @@
|
|||
updateValue(jsValue)
|
||||
}
|
||||
|
||||
const convert = () => {
|
||||
const runtime = readableToRuntimeBinding(bindings, hbsValue)
|
||||
const runtimeJs = encodeJSBinding(convertToJS(runtime))
|
||||
jsValue = runtimeToReadableBinding(bindings, runtimeJs)
|
||||
hbsValue = null
|
||||
mode = "JavaScript"
|
||||
addBinding("", { forceJS: true })
|
||||
}
|
||||
|
||||
const getHelperExample = (helper, js) => {
|
||||
let example = helper.example || ""
|
||||
if (js) {
|
||||
example = convertToJS(example).split("\n")[0].split("= ")[1]
|
||||
if (example === "null;") {
|
||||
example = ""
|
||||
}
|
||||
}
|
||||
return example || ""
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
valid = isValid(readableToRuntimeBinding(bindings, value))
|
||||
})
|
||||
|
@ -135,18 +170,21 @@
|
|||
</section>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if filteredHelpers?.length && !usingJS}
|
||||
{#if filteredHelpers?.length}
|
||||
<section>
|
||||
<div class="heading">Helpers</div>
|
||||
<ul>
|
||||
{#each filteredHelpers as helper}
|
||||
<li on:click={() => addHelper(helper)}>
|
||||
<li on:click={() => addHelper(helper, usingJS)}>
|
||||
<div class="helper">
|
||||
<div class="helper__name">{helper.displayText}</div>
|
||||
<div class="helper__description">
|
||||
{@html helper.description}
|
||||
</div>
|
||||
<pre class="helper__example">{helper.example || ""}</pre>
|
||||
<pre class="helper__example">{getHelperExample(
|
||||
helper,
|
||||
usingJS
|
||||
)}</pre>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
|
@ -172,6 +210,11 @@
|
|||
for more details.
|
||||
</p>
|
||||
{/if}
|
||||
{#if $admin.isDev}
|
||||
<div class="convert">
|
||||
<Button secondary on:click={convert}>Convert to JS</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Tab>
|
||||
{#if allowJS}
|
||||
|
@ -306,4 +349,8 @@
|
|||
color: var(--red);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.convert {
|
||||
padding-top: var(--spacing-m);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -18,10 +18,14 @@ export function addHBSBinding(value, caretPos, binding) {
|
|||
return value
|
||||
}
|
||||
|
||||
export function addJSBinding(value, caretPos, binding) {
|
||||
export function addJSBinding(value, caretPos, binding, { helper } = {}) {
|
||||
binding = typeof binding === "string" ? binding : binding.path
|
||||
value = value == null ? "" : value
|
||||
binding = `$("${binding}")`
|
||||
if (!helper) {
|
||||
binding = `$("${binding}")`
|
||||
} else {
|
||||
binding = `helper.${binding}()`
|
||||
}
|
||||
if (caretPos.start) {
|
||||
value =
|
||||
value.substring(0, caretPos.start) +
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import { datasources, integrations, queries } from "stores/backend"
|
||||
import BindingBuilder from "components/integration/QueryBindingBuilder.svelte"
|
||||
import IntegrationQueryEditor from "components/integration/index.svelte"
|
||||
import { BUDIBASE_DATASOURCE_ID } from "constants/backend"
|
||||
|
||||
export let parameters
|
||||
export let bindings = []
|
||||
|
@ -11,6 +12,10 @@
|
|||
$: datasource = $datasources.list.find(
|
||||
ds => ds._id === parameters.datasourceId
|
||||
)
|
||||
// Executequery must exclude budibase datasource
|
||||
$: executeQueryDatasources = $datasources.list.filter(
|
||||
x => x._id !== BUDIBASE_DATASOURCE_ID
|
||||
)
|
||||
|
||||
function fetchQueryDefinition(query) {
|
||||
const source = $datasources.list.find(
|
||||
|
@ -24,7 +29,7 @@
|
|||
<Select
|
||||
label="Datasource"
|
||||
bind:value={parameters.datasourceId}
|
||||
options={$datasources.list}
|
||||
options={executeQueryDatasources}
|
||||
getOptionLabel={source => source.name}
|
||||
getOptionValue={source => source._id}
|
||||
/>
|
||||
|
|
|
@ -10,23 +10,34 @@
|
|||
Input,
|
||||
Layout,
|
||||
Select,
|
||||
Label,
|
||||
} from "@budibase/bbui"
|
||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
||||
import { generate } from "shortid"
|
||||
import { LuceneUtils, Constants } from "@budibase/frontend-core"
|
||||
import { getFields } from "helpers/searchFields"
|
||||
import { createEventDispatcher, onMount } from "svelte"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let schemaFields
|
||||
export let filters = []
|
||||
export let bindings = []
|
||||
export let panel = ClientBindingPanel
|
||||
export let allowBindings = true
|
||||
export let allOr = false
|
||||
|
||||
$: dispatch("change", filters)
|
||||
$: enrichedSchemaFields = getFields(schemaFields || [])
|
||||
$: fieldOptions = enrichedSchemaFields.map(field => field.name) || []
|
||||
$: valueTypeOptions = allowBindings ? ["Value", "Binding"] : ["Value"]
|
||||
|
||||
let behaviourValue
|
||||
const behaviourOptions = [
|
||||
{ value: "and", label: "Match all of the following filters" },
|
||||
{ value: "or", label: "Match any of the following filters" },
|
||||
]
|
||||
const addFilter = () => {
|
||||
filters = [
|
||||
...filters,
|
||||
|
@ -70,7 +81,7 @@
|
|||
}
|
||||
|
||||
// if changed to an array, change default value to empty array
|
||||
const idx = filters.findIndex(x => x.field === field)
|
||||
const idx = filters.findIndex(x => x.id === expression.id)
|
||||
if (expression.type === "array") {
|
||||
filters[idx].value = []
|
||||
} else {
|
||||
|
@ -103,6 +114,10 @@
|
|||
const schema = enrichedSchemaFields.find(x => x.name === field)
|
||||
return schema?.constraints?.inclusion || []
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
behaviourValue = allOr ? "or" : "and"
|
||||
})
|
||||
</script>
|
||||
|
||||
<DrawerContent>
|
||||
|
@ -118,85 +133,101 @@
|
|||
</Body>
|
||||
{#if filters?.length}
|
||||
<div class="fields">
|
||||
{#each filters as filter, idx}
|
||||
<Select
|
||||
bind:value={filter.field}
|
||||
options={fieldOptions}
|
||||
on:change={e => onFieldChange(filter, e.detail)}
|
||||
placeholder="Column"
|
||||
/>
|
||||
<Select
|
||||
disabled={!filter.field}
|
||||
options={LuceneUtils.getValidOperatorsForType(filter.type)}
|
||||
bind:value={filter.operator}
|
||||
on:change={e => onOperatorChange(filter, e.detail)}
|
||||
placeholder={null}
|
||||
/>
|
||||
<Select
|
||||
disabled={filter.noValue || !filter.field}
|
||||
options={valueTypeOptions}
|
||||
bind:value={filter.valueType}
|
||||
placeholder={null}
|
||||
/>
|
||||
{#if filter.valueType === "Binding"}
|
||||
<DrawerBindableInput
|
||||
disabled={filter.noValue}
|
||||
title={`Value for "${filter.field}"`}
|
||||
value={filter.value}
|
||||
placeholder="Value"
|
||||
{panel}
|
||||
{bindings}
|
||||
on:change={event => (filter.value = event.detail)}
|
||||
<Select
|
||||
label="Behaviour"
|
||||
value={behaviourValue}
|
||||
options={behaviourOptions}
|
||||
getOptionLabel={opt => opt.label}
|
||||
getOptionValue={opt => opt.value}
|
||||
on:change={e => (allOr = e.detail === "or")}
|
||||
placeholder={null}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="filter-label">
|
||||
<Label>Filters</Label>
|
||||
</div>
|
||||
<div class="fields">
|
||||
{#each filters as filter, idx}
|
||||
<Select
|
||||
bind:value={filter.field}
|
||||
options={fieldOptions}
|
||||
on:change={e => onFieldChange(filter, e.detail)}
|
||||
placeholder="Column"
|
||||
/>
|
||||
{:else if ["string", "longform", "number", "formula"].includes(filter.type)}
|
||||
<Input disabled={filter.noValue} bind:value={filter.value} />
|
||||
{:else if filter.type === "array" || (filter.type === "options" && filter.operator === "oneOf")}
|
||||
<Multiselect
|
||||
disabled={filter.noValue}
|
||||
options={getFieldOptions(filter.field)}
|
||||
bind:value={filter.value}
|
||||
<Select
|
||||
disabled={!filter.field}
|
||||
options={LuceneUtils.getValidOperatorsForType(filter.type)}
|
||||
bind:value={filter.operator}
|
||||
on:change={e => onOperatorChange(filter, e.detail)}
|
||||
placeholder={null}
|
||||
/>
|
||||
{:else if filter.type === "options"}
|
||||
<Combobox
|
||||
disabled={filter.noValue}
|
||||
options={getFieldOptions(filter.field)}
|
||||
bind:value={filter.value}
|
||||
<Select
|
||||
disabled={filter.noValue || !filter.field}
|
||||
options={valueTypeOptions}
|
||||
bind:value={filter.valueType}
|
||||
placeholder={null}
|
||||
/>
|
||||
{:else if filter.type === "boolean"}
|
||||
<Combobox
|
||||
disabled={filter.noValue}
|
||||
options={[
|
||||
{ label: "True", value: "true" },
|
||||
{ label: "False", value: "false" },
|
||||
]}
|
||||
bind:value={filter.value}
|
||||
{#if filter.valueType === "Binding"}
|
||||
<DrawerBindableInput
|
||||
disabled={filter.noValue}
|
||||
title={`Value for "${filter.field}"`}
|
||||
value={filter.value}
|
||||
placeholder="Value"
|
||||
{panel}
|
||||
{bindings}
|
||||
on:change={event => (filter.value = event.detail)}
|
||||
/>
|
||||
{:else if ["string", "longform", "number", "formula"].includes(filter.type)}
|
||||
<Input disabled={filter.noValue} bind:value={filter.value} />
|
||||
{:else if filter.type === "array" || (filter.type === "options" && filter.operator === "oneOf")}
|
||||
<Multiselect
|
||||
disabled={filter.noValue}
|
||||
options={getFieldOptions(filter.field)}
|
||||
bind:value={filter.value}
|
||||
/>
|
||||
{:else if filter.type === "options"}
|
||||
<Combobox
|
||||
disabled={filter.noValue}
|
||||
options={getFieldOptions(filter.field)}
|
||||
bind:value={filter.value}
|
||||
/>
|
||||
{:else if filter.type === "boolean"}
|
||||
<Combobox
|
||||
disabled={filter.noValue}
|
||||
options={[
|
||||
{ label: "True", value: "true" },
|
||||
{ label: "False", value: "false" },
|
||||
]}
|
||||
bind:value={filter.value}
|
||||
/>
|
||||
{:else if filter.type === "datetime"}
|
||||
<DatePicker
|
||||
disabled={filter.noValue}
|
||||
enableTime={!getSchema(filter).dateOnly}
|
||||
timeOnly={getSchema(filter).timeOnly}
|
||||
bind:value={filter.value}
|
||||
/>
|
||||
{:else}
|
||||
<DrawerBindableInput disabled />
|
||||
{/if}
|
||||
<Icon
|
||||
name="Duplicate"
|
||||
hoverable
|
||||
size="S"
|
||||
on:click={() => duplicateFilter(filter.id)}
|
||||
/>
|
||||
{:else if filter.type === "datetime"}
|
||||
<DatePicker
|
||||
disabled={filter.noValue}
|
||||
enableTime={!getSchema(filter).dateOnly}
|
||||
timeOnly={getSchema(filter).timeOnly}
|
||||
bind:value={filter.value}
|
||||
<Icon
|
||||
name="Close"
|
||||
hoverable
|
||||
size="S"
|
||||
on:click={() => removeFilter(filter.id)}
|
||||
/>
|
||||
{:else}
|
||||
<DrawerBindableInput disabled />
|
||||
{/if}
|
||||
<Icon
|
||||
name="Duplicate"
|
||||
hoverable
|
||||
size="S"
|
||||
on:click={() => duplicateFilter(filter.id)}
|
||||
/>
|
||||
<Icon
|
||||
name="Close"
|
||||
hoverable
|
||||
size="S"
|
||||
on:click={() => removeFilter(filter.id)}
|
||||
/>
|
||||
{/each}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
<div class="bottom">
|
||||
<Button icon="AddCircle" size="M" secondary on:click={addFilter}>
|
||||
Add filter
|
||||
</Button>
|
||||
|
@ -219,4 +250,14 @@
|
|||
align-items: center;
|
||||
grid-template-columns: 1fr 120px 120px 1fr auto auto;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
margin-bottom: var(--spacing-s);
|
||||
}
|
||||
|
||||
.bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -8,21 +8,73 @@
|
|||
import FilterDrawer from "./FilterDrawer.svelte"
|
||||
import { currentAsset } from "builderStore"
|
||||
|
||||
const QUERY_START_REGEX = /\d[0-9]*:/g
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let value = []
|
||||
export let componentInstance
|
||||
export let bindings = []
|
||||
|
||||
let drawer
|
||||
let tempValue = value || []
|
||||
let drawer,
|
||||
toSaveFilters = null,
|
||||
allOr,
|
||||
initialAllOr
|
||||
|
||||
$: initialFilters = correctFilters(value || [])
|
||||
$: dataSource = getDatasourceForProvider($currentAsset, componentInstance)
|
||||
$: schema = getSchemaForDatasource($currentAsset, dataSource)?.schema
|
||||
$: schemaFields = Object.values(schema || {})
|
||||
|
||||
const saveFilter = async () => {
|
||||
dispatch("change", tempValue)
|
||||
function addNumbering(filters) {
|
||||
let count = 1
|
||||
for (let value of filters) {
|
||||
if (value.field && value.field?.match(QUERY_START_REGEX) == null) {
|
||||
value.field = `${count++}:${value.field}`
|
||||
}
|
||||
}
|
||||
return filters
|
||||
}
|
||||
|
||||
function correctFilters(filters) {
|
||||
const corrected = []
|
||||
for (let filter of filters) {
|
||||
let field = filter.field
|
||||
if (filter.operator === "allOr") {
|
||||
initialAllOr = allOr = true
|
||||
continue
|
||||
}
|
||||
if (
|
||||
typeof filter.field === "string" &&
|
||||
filter.field.match(QUERY_START_REGEX) != null
|
||||
) {
|
||||
const parts = field.split(":")
|
||||
const number = parts[0]
|
||||
// it's the new format, remove number
|
||||
if (!isNaN(parseInt(number))) {
|
||||
parts.shift()
|
||||
field = parts.join(":")
|
||||
}
|
||||
}
|
||||
corrected.push({
|
||||
...filter,
|
||||
field,
|
||||
})
|
||||
}
|
||||
return corrected
|
||||
}
|
||||
|
||||
async function saveFilter() {
|
||||
if (!toSaveFilters && allOr !== initialAllOr) {
|
||||
toSaveFilters = initialFilters
|
||||
}
|
||||
const filters = toSaveFilters?.filter(filter => filter.operator !== "allOr")
|
||||
if (allOr && filters) {
|
||||
filters.push({ operator: "allOr" })
|
||||
}
|
||||
// only save if anything was updated
|
||||
if (filters) {
|
||||
dispatch("change", addNumbering(filters))
|
||||
}
|
||||
notifications.success("Filters saved.")
|
||||
drawer.hide()
|
||||
}
|
||||
|
@ -33,8 +85,12 @@
|
|||
<Button cta slot="buttons" on:click={saveFilter}>Save</Button>
|
||||
<FilterDrawer
|
||||
slot="body"
|
||||
bind:filters={tempValue}
|
||||
filters={initialFilters}
|
||||
{bindings}
|
||||
{schemaFields}
|
||||
bind:allOr
|
||||
on:change={event => {
|
||||
toSaveFilters = event.detail
|
||||
}}
|
||||
/>
|
||||
</Drawer>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { Layout, Icon, ActionButton } from "@budibase/bbui"
|
||||
import { Layout, Icon, ActionButton, InlineAlert } from "@budibase/bbui"
|
||||
import StatusRenderer from "./StatusRenderer.svelte"
|
||||
import DateTimeRenderer from "components/common/renderers/DateTimeRenderer.svelte"
|
||||
import TestDisplay from "components/automation/AutomationBuilder/TestDisplay.svelte"
|
||||
|
@ -9,6 +9,7 @@
|
|||
export let history
|
||||
export let appId
|
||||
export let close
|
||||
const STOPPED_ERROR = "stopped_error"
|
||||
|
||||
$: exists = $automationStore.automations?.find(
|
||||
auto => auto._id === history?.automationId
|
||||
|
@ -32,6 +33,15 @@
|
|||
<Icon name="JourneyVoyager" />
|
||||
<div>{history.automationName}</div>
|
||||
</div>
|
||||
{#if history.status === STOPPED_ERROR}
|
||||
<div class="cron-error">
|
||||
<InlineAlert
|
||||
type="error"
|
||||
header="CRON automation disabled"
|
||||
message="Fix the error and re-publish your app to re-activate."
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
{#if exists}
|
||||
<ActionButton
|
||||
|
@ -87,4 +97,10 @@
|
|||
grid-template-columns: 1fr auto;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
|
||||
.cron-error {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
export let value
|
||||
|
||||
$: isError = !value || value.toLowerCase() === "error"
|
||||
$: isStopped = value?.toLowerCase() === "stopped"
|
||||
$: isStoppedError = value?.toLowerCase() === "stopped_error"
|
||||
$: isStopped = value?.toLowerCase() === "stopped" || isStoppedError
|
||||
$: status = getStatus(isError, isStopped)
|
||||
|
||||
function getStatus(error, stopped) {
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
<div class="desktop">
|
||||
<AppLockModal {app} buttonSize="M" />
|
||||
<span><AppLockModal {app} buttonSize="M" /></span>
|
||||
</div>
|
||||
<div class="desktop">
|
||||
<div class="app-status">
|
||||
|
|
|
@ -163,6 +163,8 @@ export const SWITCHABLE_TYPES = [
|
|||
...ALLOWABLE_NUMBER_TYPES,
|
||||
]
|
||||
|
||||
export const BUDIBASE_DATASOURCE_ID = "bb_internal"
|
||||
|
||||
export const IntegrationTypes = {
|
||||
POSTGRES: "POSTGRES",
|
||||
MONGODB: "MONGODB",
|
||||
|
|
|
@ -3,6 +3,7 @@ import { get } from "svelte/store"
|
|||
|
||||
export const FEATURE_FLAGS = {
|
||||
LICENSING: "LICENSING",
|
||||
USER_GROUPS: "USER_GROUPS",
|
||||
}
|
||||
|
||||
export const isEnabled = featureFlag => {
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import { admin, auth } from "stores/portal"
|
||||
import { onMount } from "svelte"
|
||||
import { CookieUtils, Constants } from "@budibase/frontend-core"
|
||||
import { API } from "api"
|
||||
|
||||
let loaded = false
|
||||
|
||||
|
@ -53,6 +54,9 @@
|
|||
await auth.setOrganisation(urlTenantId)
|
||||
}
|
||||
}
|
||||
async function analyticsPing() {
|
||||
await API.analyticsPing({ source: "builder" })
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
|
@ -73,6 +77,9 @@
|
|||
// being logged in
|
||||
}
|
||||
loaded = true
|
||||
|
||||
// lastly
|
||||
await analyticsPing()
|
||||
})
|
||||
|
||||
$: {
|
||||
|
|
|
@ -55,13 +55,16 @@
|
|||
let saveId, url
|
||||
let response, schema, enabledHeaders
|
||||
let authConfigId
|
||||
let dynamicVariables, addVariableModal, varBinding
|
||||
let dynamicVariables, addVariableModal, varBinding, globalDynamicBindings
|
||||
let restBindings = getRestBindings()
|
||||
|
||||
$: staticVariables = datasource?.config?.staticVariables || {}
|
||||
|
||||
$: customRequestBindings = toBindingsArray(requestBindings, "Binding")
|
||||
$: dynamicRequestBindings = toBindingsArray(dynamicVariables, "Dynamic")
|
||||
$: globalDynamicRequestBindings = toBindingsArray(
|
||||
globalDynamicBindings,
|
||||
"Dynamic"
|
||||
)
|
||||
$: dataSourceStaticBindings = toBindingsArray(
|
||||
staticVariables,
|
||||
"Datasource.Static"
|
||||
|
@ -70,7 +73,7 @@
|
|||
$: mergedBindings = [
|
||||
...restBindings,
|
||||
...customRequestBindings,
|
||||
...dynamicRequestBindings,
|
||||
...globalDynamicRequestBindings,
|
||||
...dataSourceStaticBindings,
|
||||
]
|
||||
|
||||
|
@ -231,11 +234,11 @@
|
|||
]
|
||||
|
||||
// convert dynamic variables list to simple key/val object
|
||||
const getDynamicVariables = (datasource, queryId) => {
|
||||
const getDynamicVariables = (datasource, queryId, matchFn) => {
|
||||
const variablesList = datasource?.config?.dynamicVariables
|
||||
if (variablesList && variablesList.length > 0) {
|
||||
const filtered = queryId
|
||||
? variablesList.filter(variable => variable.queryId === queryId)
|
||||
? variablesList.filter(variable => matchFn(variable, queryId))
|
||||
: variablesList
|
||||
return filtered.reduce(
|
||||
(acc, next) => ({ ...acc, [next.name]: next.value }),
|
||||
|
@ -367,12 +370,21 @@
|
|||
if (query && !query.fields.pagination) {
|
||||
query.fields.pagination = {}
|
||||
}
|
||||
dynamicVariables = getDynamicVariables(datasource, query._id)
|
||||
dynamicVariables = getDynamicVariables(
|
||||
datasource,
|
||||
query._id,
|
||||
(variable, queryId) => variable.queryId === queryId
|
||||
)
|
||||
globalDynamicBindings = getDynamicVariables(
|
||||
datasource,
|
||||
query._id,
|
||||
(variable, queryId) => variable.queryId !== queryId
|
||||
)
|
||||
|
||||
prettifyQueryRequestBody(
|
||||
query,
|
||||
requestBindings,
|
||||
dynamicVariables,
|
||||
globalDynamicBindings,
|
||||
staticVariables,
|
||||
restBindings
|
||||
)
|
||||
|
@ -437,7 +449,7 @@
|
|||
valuePlaceholder="Default"
|
||||
bindings={[
|
||||
...restBindings,
|
||||
...dynamicRequestBindings,
|
||||
...globalDynamicRequestBindings,
|
||||
...dataSourceStaticBindings,
|
||||
]}
|
||||
bindingDrawerLeft="260px"
|
||||
|
|
|
@ -186,7 +186,7 @@
|
|||
$goto("./navigation")
|
||||
}
|
||||
} else if (type === "request-add-component") {
|
||||
$goto("./components/new")
|
||||
$goto(`./components/${$selectedComponent?._id}/new`)
|
||||
} else if (type === "highlight-setting") {
|
||||
store.actions.settings.highlight(data.setting)
|
||||
|
||||
|
|
|
@ -184,6 +184,7 @@
|
|||
<div class="category-label">{category.name}</div>
|
||||
{#each category.children as component}
|
||||
<div
|
||||
data-cy={`component-${component.name}`}
|
||||
class="component"
|
||||
class:selected={selectedIndex ===
|
||||
orderMap[component.component]}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue