diff --git a/.all-contributorsrc b/.all-contributorsrc index 53705907c2..3a416f917e 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -162,6 +162,7 @@ "translation" ] }, + { "login": "mslourens", "name": "Maurits Lourens", "avatar_url": "https://avatars.githubusercontent.com/u/1907152?v=4", diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml index 0fb8a5fea0..b37ff9cee8 100644 --- a/.github/workflows/deploy-release.yml +++ b/.github/workflows/deploy-release.yml @@ -69,6 +69,28 @@ jobs: env: KUBECONFIG_FILE: '${{ secrets.RELEASE_KUBECONFIG }}' + - name: Re roll app-service + uses: actions-hub/kubectl@master + env: + KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }} + with: + args: rollout restart deployment app-service -n budibase + + - name: Re roll proxy-service + uses: actions-hub/kubectl@master + env: + KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }} + with: + args: rollout restart deployment proxy-service -n budibase + + - name: Re roll worker-service + uses: actions-hub/kubectl@master + env: + KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }} + with: + args: rollout restart deployment worker-service -n budibase + + - name: Discord Webhook Action uses: tsickert/discord-webhook@v4.0.0 with: diff --git a/.github/workflows/release-develop.yml b/.github/workflows/release-develop.yml index 631308d945..57e65c734e 100644 --- a/.github/workflows/release-develop.yml +++ b/.github/workflows/release-develop.yml @@ -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 @@ -119,6 +120,27 @@ jobs: ] env: KUBECONFIG_FILE: '${{ secrets.RELEASE_KUBECONFIG }}' + + - name: Re roll app-service + uses: actions-hub/kubectl@master + env: + KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }} + with: + args: rollout restart deployment app-service -n budibase + + - name: Re roll proxy-service + uses: actions-hub/kubectl@master + env: + KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }} + with: + args: rollout restart deployment proxy-service -n budibase + + - name: Re roll worker-service + uses: actions-hub/kubectl@master + env: + KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }} + with: + args: rollout restart deployment worker-service -n budibase - name: Discord Webhook Action uses: tsickert/discord-webhook@v4.0.0 diff --git a/.github/workflows/smoke_test.yaml b/.github/workflows/smoke_test.yaml index 7002c8335b..cffb914aaf 100644 --- a/.github/workflows/smoke_test.yaml +++ b/.github/workflows/smoke_test.yaml @@ -1,4 +1,4 @@ -name: Budibase Smoke Test +name: Budibase Nightly Tests on: workflow_dispatch: @@ -6,7 +6,7 @@ on: - cron: "0 5 * * *" # every day at 5AM jobs: - release: + nightly: runs-on: ubuntu-latest steps: @@ -43,6 +43,18 @@ jobs: name: Test Reports path: packages/builder/cypress/reports/testReport.html + # TODO: enable once running in QA test env + # - name: Configure AWS Credentials + # uses: aws-actions/configure-aws-credentials@v1 + # with: + # aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + # aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + # aws-region: eu-west-1 + + # - name: Upload test results HTML + # uses: aws-actions/configure-aws-credentials@v1 + # run: aws s3 cp packages/builder/cypress/reports/testReport.html s3://{{ secrets.BUDI_QA_REPORTS_BUCKET_NAME }}/$GITHUB_RUN_ID/index.html + - name: Cypress Discord Notify run: yarn test:e2e:ci:notify env: diff --git a/.prettierrc.json b/.prettierrc.json index 39654fd9f9..dae5906124 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -4,7 +4,7 @@ "singleQuote": false, "trailingComma": "es5", "arrowParens": "avoid", - "jsxBracketSameLine": false, + "bracketSameLine": false, "plugins": ["prettier-plugin-svelte"], "svelteSortOrder": "options-scripts-markup-styles" } diff --git a/charts/budibase/templates/app-service-deployment.yaml b/charts/budibase/templates/app-service-deployment.yaml index 0f8e2a08ce..74b98ac008 100644 --- a/charts/budibase/templates/app-service-deployment.yaml +++ b/charts/budibase/templates/app-service-deployment.yaml @@ -134,6 +134,18 @@ spec: - name: NODE_DEBUG value: {{ .Values.services.apps.nodeDebug | quote }} {{ end }} + {{ if .Values.globals.elasticApmEnabled }} + - name: ELASTIC_APM_ENABLED + value: {{ .Values.globals.elasticApmEnabled | quote }} + {{ end }} + {{ if .Values.globals.elasticApmSecretToken }} + - name: ELASTIC_APM_SECRET_TOKEN + value: {{ .Values.globals.elasticApmSecretToken | quote }} + {{ end }} + {{ if .Values.globals.elasticApmServerUrl }} + - name: ELASTIC_APM_SERVER_URL + value: {{ .Values.globals.elasticApmServerUrl | quote }} + {{ end }} image: budibase/apps:{{ .Values.globals.appVersion }} imagePullPolicy: Always diff --git a/charts/budibase/templates/worker-service-deployment.yaml b/charts/budibase/templates/worker-service-deployment.yaml index 918dab427b..083231eeff 100644 --- a/charts/budibase/templates/worker-service-deployment.yaml +++ b/charts/budibase/templates/worker-service-deployment.yaml @@ -27,6 +27,8 @@ spec: spec: containers: - env: + - name: BUDIBASE_ENVIRONMENT + value: {{ .Values.globals.budibaseEnv }} - name: DEPLOYMENT_ENVIRONMENT value: "kubernetes" - name: CLUSTER_PORT @@ -125,6 +127,19 @@ spec: value: {{ .Values.globals.google.secret | quote }} - name: TENANT_FEATURE_FLAGS value: {{ .Values.globals.tenantFeatureFlags | quote }} + {{ if .Values.globals.elasticApmEnabled }} + - name: ELASTIC_APM_ENABLED + value: {{ .Values.globals.elasticApmEnabled | quote }} + {{ end }} + {{ if .Values.globals.elasticApmSecretToken }} + - name: ELASTIC_APM_SECRET_TOKEN + value: {{ .Values.globals.elasticApmSecretToken | quote }} + {{ end }} + {{ if .Values.globals.elasticApmServerUrl }} + - name: ELASTIC_APM_SERVER_URL + value: {{ .Values.globals.elasticApmServerUrl | quote }} + {{ end }} + image: budibase/worker:{{ .Values.globals.appVersion }} imagePullPolicy: Always livenessProbe: diff --git a/charts/budibase/values.yaml b/charts/budibase/values.yaml index 26f8cd24ed..9b5e76d0d7 100644 --- a/charts/budibase/values.yaml +++ b/charts/budibase/values.yaml @@ -114,6 +114,10 @@ globals: smtp: enabled: false +# elasticApmEnabled: +# elasticApmSecretToken: +# elasticApmServerUrl: + services: budibaseVersion: latest dns: cluster.local diff --git a/hosting/nginx.dev.conf.hbs b/hosting/nginx.dev.conf.hbs index 9398b7e719..20c4d3d182 100644 --- a/hosting/nginx.dev.conf.hbs +++ b/hosting/nginx.dev.conf.hbs @@ -15,7 +15,10 @@ http { log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; + '"$http_user_agent" "$http_x_forwarded_for" ' + 'response_time=$upstream_response_time proxy_host=$proxy_host upstream_addr=$upstream_addr'; + + access_log /var/log/nginx/access.log main; map $http_upgrade $connection_upgrade { default "upgrade"; @@ -62,6 +65,10 @@ http { proxy_pass http://{{ address }}:4001; } + location /preview { + proxy_pass http://{{ address }}:4001; + } + location /builder { proxy_pass http://{{ address }}:3000; rewrite ^/builder(.*)$ /builder/$1 break; diff --git a/hosting/nginx.prod.conf.hbs b/hosting/nginx.prod.conf.hbs index 4213626309..5ecea67c42 100644 --- a/hosting/nginx.prod.conf.hbs +++ b/hosting/nginx.prod.conf.hbs @@ -33,7 +33,10 @@ http { log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; + '"$http_user_agent" "$http_x_forwarded_for" ' + 'response_time=$upstream_response_time proxy_host=$proxy_host upstream_addr=$upstream_addr'; + + access_log /var/log/nginx/access.log main; map $http_upgrade $connection_upgrade { default "upgrade"; @@ -85,6 +88,10 @@ http { proxy_pass http://$apps:4002; } + location /preview { + proxy_pass http://$apps:4002; + } + location = / { proxy_pass http://$apps:4002; } @@ -94,6 +101,7 @@ http { proxy_pass http://$watchtower:8080; } {{/if}} + location ~ ^/(builder|app_) { proxy_http_version 1.1; proxy_set_header Connection $connection_upgrade; diff --git a/hosting/scripts/build-target-paths.sh b/hosting/scripts/build-target-paths.sh index 4c165d12e7..ee314c1ce4 100644 --- a/hosting/scripts/build-target-paths.sh +++ b/hosting/scripts/build-target-paths.sh @@ -3,15 +3,18 @@ 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#/home#g" /opt/clouseau/clouseau.ini + sed -i "s#DATA_DIR#/home#g" /opt/couchdb/etc/local.ini +else + sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini + sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini + +fi \ No newline at end of file diff --git a/hosting/single/Dockerfile b/hosting/single/Dockerfile index b631782ac2..476a6e5e94 100644 --- a/hosting/single/Dockerfile +++ b/hosting/single/Dockerfile @@ -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,6 +35,7 @@ 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_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU \ @@ -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 diff --git a/hosting/single/clouseau/clouseau.ini b/hosting/single/clouseau/clouseau.ini index 78e43744e5..578a5acafa 100644 --- a/hosting/single/clouseau/clouseau.ini +++ b/hosting/single/clouseau/clouseau.ini @@ -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 diff --git a/hosting/single/couch/local.ini b/hosting/single/couch/local.ini index 72872a60e1..35f0383dfc 100644 --- a/hosting/single/couch/local.ini +++ b/hosting/single/couch/local.ini @@ -1,5 +1,5 @@ ; CouchDB Configuration Settings [couchdb] -database_dir = /data/couch/dbs -view_index_dir = /data/couch/views +database_dir = DATA_DIR/couchdb/dbs +view_index_dir = DATA_DIR/couchdb/views diff --git a/hosting/single/healthcheck.sh b/hosting/single/healthcheck.sh index b92cd153a3..592b3e94fa 100644 --- a/hosting/single/healthcheck.sh +++ b/hosting/single/healthcheck.sh @@ -3,6 +3,11 @@ healthy=true if [ -f "/data/.env" ]; then export $(cat /data/.env | xargs) +elif [ -f "/home/.env" ]; then + export $(cat /home/.env | xargs) +else + echo "No .env file found" + healthy=false fi if [[ $(curl -Lfk -s -w "%{http_code}\n" http://localhost/ -o /dev/null) -ne 200 ]]; then diff --git a/hosting/single/runner.sh b/hosting/single/runner.sh index 9abb2fd093..09387343ba 100644 --- a/hosting/single/runner.sh +++ b/hosting/single/runner.sh @@ -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 diff --git a/lerna.json b/lerna.json index 88627b68e4..bb19f48bb0 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.2.47", + "version": "1.2.57", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index d999483139..a35758d4cf 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "1.2.47", + "version": "1.2.57", "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.2.47", + "@budibase/types": "^1.2.57", "@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", diff --git a/packages/backend-core/src/db/utils.ts b/packages/backend-core/src/db/utils.ts index 41479c2b31..321ebd7f58 100644 --- a/packages/backend-core/src/db/utils.ts +++ b/packages/backend-core/src/db/utils.ts @@ -233,6 +233,10 @@ 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] === DocumentType.APP) { diff --git a/packages/backend-core/src/middleware/joi-validator.js b/packages/backend-core/src/middleware/joi-validator.js index 1686b0e727..748ccebd89 100644 --- a/packages/backend-core/src/middleware/joi-validator.js +++ b/packages/backend-core/src/middleware/joi-validator.js @@ -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}`) diff --git a/packages/backend-core/src/objectStore/index.ts b/packages/backend-core/src/objectStore/index.ts index 503ab9bca0..1b880ef7b2 100644 --- a/packages/backend-core/src/objectStore/index.ts +++ b/packages/backend-core/src/objectStore/index.ts @@ -66,15 +66,13 @@ const PUBLIC_BUCKETS = [ObjectStoreBuckets.APPS, ObjectStoreBuckets.GLOBAL] * @constructor */ export const ObjectStore = (bucket: any) => { - AWS.config.update({ - accessKeyId: env.MINIO_ACCESS_KEY, - secretAccessKey: env.MINIO_SECRET_KEY, - region: env.AWS_REGION, - }) const config: any = { s3ForcePathStyle: true, signatureVersion: "v4", apiVersion: "2006-03-01", + accessKeyId: env.MINIO_ACCESS_KEY, + secretAccessKey: env.MINIO_SECRET_KEY, + region: env.AWS_REGION, } if (bucket) { config.params = { diff --git a/packages/backend-core/yarn.lock b/packages/backend-core/yarn.lock index e1f38a798f..9f71691f44 100644 --- a/packages/backend-core/yarn.lock +++ b/packages/backend-core/yarn.lock @@ -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" diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 072dd0784a..5a3a1b4b97 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "1.2.47", + "version": "1.2.57", "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.2.47", + "@budibase/string-templates": "^1.2.57", "@spectrum-css/actionbutton": "^1.0.1", "@spectrum-css/actiongroup": "^1.0.1", "@spectrum-css/avatar": "^3.0.2", diff --git a/packages/bbui/src/Form/Core/DatePicker.svelte b/packages/bbui/src/Form/Core/DatePicker.svelte index c6230c5212..1a7ab59818 100644 --- a/packages/bbui/src/Form/Core/DatePicker.svelte +++ b/packages/bbui/src/Form/Core/DatePicker.svelte @@ -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 @@ -149,7 +157,7 @@ } -{#key timeOnly} +{#key redrawOptions} idx !== selectedImageIdx) ) + if (deleteAttachments) { + await deleteAttachments( + value.filter((x, idx) => idx === selectedImageIdx).map(item => item.key) + ) + } selectedImageIdx = 0 } diff --git a/packages/bbui/src/Form/Core/Multiselect.svelte b/packages/bbui/src/Form/Core/Multiselect.svelte index 9dd5a25a4f..eb39e39042 100644 --- a/packages/bbui/src/Form/Core/Multiselect.svelte +++ b/packages/bbui/src/Form/Core/Multiselect.svelte @@ -23,7 +23,7 @@ $: toggleOption = makeToggleOption(selectedLookupMap, value) const getFieldText = (value, map, placeholder) => { - if (value?.length) { + if (Array.isArray(value) && value.length > 0) { if (!map) { return "" } @@ -36,7 +36,7 @@ const getSelectedLookupMap = value => { let map = {} - if (value?.length) { + if (Array.isArray(value) && value.length > 0) { value.forEach(option => { if (option) { map[option] = true diff --git a/packages/bbui/src/Form/DatePicker.svelte b/packages/bbui/src/Form/DatePicker.svelte index a4b2379782..a0b102dbe8 100644 --- a/packages/bbui/src/Form/DatePicker.svelte +++ b/packages/bbui/src/Form/DatePicker.svelte @@ -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} diff --git a/packages/bbui/src/Form/Dropzone.svelte b/packages/bbui/src/Form/Dropzone.svelte index f1b548f7f1..5b82c0ebea 100644 --- a/packages/bbui/src/Form/Dropzone.svelte +++ b/packages/bbui/src/Form/Dropzone.svelte @@ -10,6 +10,7 @@ export let error = null export let fileSizeLimit = undefined export let processFiles = undefined + export let deleteAttachments = undefined export let handleFileTooLarge = undefined export let handleTooManyFiles = undefined export let gallery = true @@ -30,6 +31,7 @@ {value} {fileSizeLimit} {processFiles} + {deleteAttachments} {handleFileTooLarge} {handleTooManyFiles} {gallery} diff --git a/packages/bbui/src/Icon/Icon.svelte b/packages/bbui/src/Icon/Icon.svelte index 9c99178fdb..f2cae14f0b 100644 --- a/packages/bbui/src/Icon/Icon.svelte +++ b/packages/bbui/src/Icon/Icon.svelte @@ -83,4 +83,9 @@ transform: translateX(-50%); text-align: center; } + + .spectrum-Icon--sizeXS { + width: 10px; + height: 10px; + } diff --git a/packages/bbui/src/Menu/Item.svelte b/packages/bbui/src/Menu/Item.svelte index a5609683a8..dfe61c1736 100644 --- a/packages/bbui/src/Menu/Item.svelte +++ b/packages/bbui/src/Menu/Item.svelte @@ -1,5 +1,6 @@ - + Import diff --git a/packages/builder/src/components/common/AppLockModal.svelte b/packages/builder/src/components/common/AppLockModal.svelte index 5ca35f05db..9794e350d9 100644 --- a/packages/builder/src/components/common/AppLockModal.svelte +++ b/packages/builder/src/components/common/AppLockModal.svelte @@ -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} - - -

- Apps are locked to prevent work from being lost from overlapping changes - between your team. -

- - {#if lockedByYou && getExpiryDuration(app) > 0} - - {processStringSync( - "This lock will expire in {{ duration time 'millisecond' }} from now.", - { - time: getExpiryDuration(app), - } - )} - - {/if} -
- - - {#if lockedByYou} - - {/if} - -
-
-
+{#key app} +
+ + + + + Apps are locked to prevent work from being lost from overlapping + changes between your team. + + {#if lockedByYou && getExpiryDuration(app) > 0} + + {processStringSync( + "This lock will expire in {{ duration time 'millisecond' }} from now. This lock will expire in This lock will expire in ", + { + time: getExpiryDuration(app), + } + )} + + {/if} +
+ + + {#if lockedByYou} + + {/if} + +
+
+
+
+
+{/key} diff --git a/packages/builder/src/components/common/bindings/utils.js b/packages/builder/src/components/common/bindings/utils.js index 42a3f11677..c7b40604ad 100644 --- a/packages/builder/src/components/common/bindings/utils.js +++ b/packages/builder/src/components/common/bindings/utils.js @@ -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) + diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ShowNotification.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ShowNotification.svelte new file mode 100644 index 0000000000..55b00d215d --- /dev/null +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ShowNotification.svelte @@ -0,0 +1,61 @@ + + +
+ + onFieldChange(filter, e.detail)} - placeholder="Column" - /> - - {#if filter.valueType === "Binding"} - (filter.value = event.detail)} + onFieldChange(filter, e.detail)} + placeholder="Column" /> - {:else if ["string", "longform", "number", "formula"].includes(filter.type)} - - {:else if ["options", "array"].includes(filter.type)} - onOperatorChange(filter, e.detail)} + placeholder={null} /> - {:else if filter.type === "boolean"} - - {:else if filter.type === "datetime"} - (filter.value = event.detail)} + /> + {:else if ["string", "longform", "number", "formula"].includes(filter.type)} + + {:else if filter.type === "array" || (filter.type === "options" && filter.operator === "oneOf")} + + {:else if filter.type === "options"} + + {:else if filter.type === "boolean"} + + {:else if filter.type === "datetime"} + + {:else} + + {/if} + duplicateFilter(filter.id)} /> - {:else} - - {/if} - duplicateFilter(filter.id)} - /> - removeFilter(filter.id)} - /> - {/each} + removeFilter(filter.id)} + /> + {/each} +
{/if} -
+
@@ -202,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; + } diff --git a/packages/builder/src/components/design/settings/controls/FilterEditor/FilterEditor.svelte b/packages/builder/src/components/design/settings/controls/FilterEditor/FilterEditor.svelte index 2cb35a9cf5..ea54afc0ee 100644 --- a/packages/builder/src/components/design/settings/controls/FilterEditor/FilterEditor.svelte +++ b/packages/builder/src/components/design/settings/controls/FilterEditor/FilterEditor.svelte @@ -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 @@ { + toSaveFilters = event.detail + }} /> diff --git a/packages/builder/src/components/start/AppRow.svelte b/packages/builder/src/components/start/AppRow.svelte index 49f99c9f77..91920073bb 100644 --- a/packages/builder/src/components/start/AppRow.svelte +++ b/packages/builder/src/components/start/AppRow.svelte @@ -30,7 +30,7 @@ {/if}
- +
diff --git a/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/rest/[query]/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/rest/[query]/index.svelte index 5ccc173318..b0a93f8eec 100644 --- a/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/rest/[query]/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/rest/[query]/index.svelte @@ -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" diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte index 814930d636..eb8743bf39 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte @@ -8,7 +8,6 @@ selectedLayout, currentAsset, } from "builderStore" - import iframeTemplate from "./iframeTemplate" import ConfirmDialog from "components/common/ConfirmDialog.svelte" import { ProgressCircle, @@ -40,12 +39,6 @@ BUDIBASE: "type", } - // Construct iframe template - $: template = iframeTemplate.replace( - /\{\{ CLIENT_LIB_PATH }}/, - $store.clientLibPath - ) - const placeholderScreen = new Screen() .name("Screen Placeholder") .route("/") @@ -151,7 +144,11 @@ } else if (type === "update-prop") { await store.actions.components.updateSetting(data.prop, data.value) } else if (type === "delete-component" && data.id) { + // Legacy type, can be deleted in future confirmDeleteComponent(data.id) + } else if (type === "key-down") { + const { key, ctrlKey } = data + document.dispatchEvent(new KeyboardEvent("keydown", { key, ctrlKey })) } else if (type === "duplicate-component" && data.id) { const rootComponent = get(currentAsset).props const component = findComponent(rootComponent, data.id) @@ -293,7 +290,7 @@