diff --git a/.eslintrc.json b/.eslintrc.json index f6f03c6523..79f7e56712 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -57,7 +57,10 @@ "destructuredArrayIgnorePattern": "^_" } ], - "import/no-relative-packages": "error" + "import/no-relative-packages": "error", + "import/export": "error", + "import/no-duplicates": "error", + "import/newline-after-import": "error" }, "globals": { "GeolocationPositionError": true diff --git a/hosting/couchdb/Dockerfile b/hosting/couchdb/Dockerfile index 792856cac7..f83df7038b 100644 --- a/hosting/couchdb/Dockerfile +++ b/hosting/couchdb/Dockerfile @@ -29,7 +29,6 @@ WORKDIR /opt/couchdb ADD couch/vm.args couch/local.ini ./etc/ WORKDIR / -ADD build-target-paths.sh . ADD runner.sh ./bbcouch-runner.sh -RUN chmod +x ./bbcouch-runner.sh /opt/clouseau/bin/clouseau ./build-target-paths.sh +RUN chmod +x ./bbcouch-runner.sh /opt/clouseau/bin/clouseau CMD ["./bbcouch-runner.sh"] diff --git a/hosting/couchdb/build-target-paths.sh b/hosting/couchdb/build-target-paths.sh deleted file mode 100644 index 34227011f4..0000000000 --- a/hosting/couchdb/build-target-paths.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash - -echo ${TARGETBUILD} > /buildtarget.txt -if [[ "${TARGETBUILD}" = "aas" ]]; then - # Azure AppService uses /home for persistent data & SSH on port 2222 - DATA_DIR="${DATA_DIR:-/home}" - WEBSITES_ENABLE_APP_SERVICE_STORAGE=true - mkdir -p $DATA_DIR/{search,minio,couch} - mkdir -p $DATA_DIR/couch/{dbs,views} - chown -R couchdb:couchdb $DATA_DIR/couch/ - apt update - apt-get install -y openssh-server - echo "root:Docker!" | chpasswd - mkdir -p /tmp - chmod +x /tmp/ssh_setup.sh \ - && (sleep 1;/tmp/ssh_setup.sh 2>&1 > /dev/null) - cp /etc/sshd_config /etc/ssh/sshd_config - /etc/init.d/ssh restart - 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/couchdb/runner.sh b/hosting/couchdb/runner.sh index 4102d2a751..2e4d26122f 100644 --- a/hosting/couchdb/runner.sh +++ b/hosting/couchdb/runner.sh @@ -1,14 +1,52 @@ #!/bin/bash DATA_DIR=${DATA_DIR:-/data} + mkdir -p ${DATA_DIR} mkdir -p ${DATA_DIR}/couch/{dbs,views} mkdir -p ${DATA_DIR}/search chown -R couchdb:couchdb ${DATA_DIR}/couch -/build-target-paths.sh + +echo ${TARGETBUILD} > /buildtarget.txt +if [[ "${TARGETBUILD}" = "aas" ]]; then + # Azure AppService uses /home for persistent data & SSH on port 2222 + DATA_DIR="${DATA_DIR:-/home}" + WEBSITES_ENABLE_APP_SERVICE_STORAGE=true + mkdir -p $DATA_DIR/{search,minio,couch} + mkdir -p $DATA_DIR/couch/{dbs,views} + chown -R couchdb:couchdb $DATA_DIR/couch/ + apt update + apt-get install -y openssh-server + echo "root:Docker!" | chpasswd + mkdir -p /tmp + chmod +x /tmp/ssh_setup.sh \ + && (sleep 1;/tmp/ssh_setup.sh 2>&1 > /dev/null) + cp /etc/sshd_config /etc/ssh/sshd_config + /etc/init.d/ssh restart + sed -i "s#DATA_DIR#/home#g" /opt/clouseau/clouseau.ini + sed -i "s#DATA_DIR#/home#g" /opt/couchdb/etc/local.ini +elif [[ "${TARGETBUILD}" = "single" ]]; then + sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini + sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini +elif [[ -n $KUBERNETES_SERVICE_HOST ]]; then + # In Kubernetes the directory /opt/couchdb/data has a persistent volume + # mount for storing database data. + sed -i "s#DATA_DIR#/opt/couchdb/data#g" /opt/clouseau/clouseau.ini + sed -i "s#DATA_DIR#/opt/couchdb/data#g" /opt/couchdb/etc/local.ini + sed -i "s/^-name .*$//g" /opt/couchdb/etc/vm.args +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 + /opt/clouseau/bin/clouseau > /dev/stdout 2>&1 & /docker-entrypoint.sh /opt/couchdb/bin/couchdb & -sleep 10 + +while [[ $(curl -s -w "%{http_code}\n" http://localhost:5984/_up -o /dev/null) -ne 200 ]]; do + echo 'Waiting for CouchDB to start...'; + sleep 5; +done + curl -X PUT http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984/_users curl -X PUT http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984/_replicator sleep infinity \ No newline at end of file diff --git a/hosting/docker-compose.build.yaml b/hosting/docker-compose.build.yaml index 7ead001a1c..dbc3613599 100644 --- a/hosting/docker-compose.build.yaml +++ b/hosting/docker-compose.build.yaml @@ -6,7 +6,7 @@ services: app-service: build: context: .. - dockerfile: packages/server/Dockerfile.v2 + dockerfile: packages/server/Dockerfile args: - BUDIBASE_VERSION=0.0.0+dev-docker container_name: build-bbapps @@ -36,7 +36,7 @@ services: worker-service: build: context: .. - dockerfile: packages/worker/Dockerfile.v2 + dockerfile: packages/worker/Dockerfile args: - BUDIBASE_VERSION=0.0.0+dev-docker container_name: build-bbworker diff --git a/hosting/scripts/build-target-paths.sh b/hosting/scripts/build-target-paths.sh deleted file mode 100644 index 34227011f4..0000000000 --- a/hosting/scripts/build-target-paths.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash - -echo ${TARGETBUILD} > /buildtarget.txt -if [[ "${TARGETBUILD}" = "aas" ]]; then - # Azure AppService uses /home for persistent data & SSH on port 2222 - DATA_DIR="${DATA_DIR:-/home}" - WEBSITES_ENABLE_APP_SERVICE_STORAGE=true - mkdir -p $DATA_DIR/{search,minio,couch} - mkdir -p $DATA_DIR/couch/{dbs,views} - chown -R couchdb:couchdb $DATA_DIR/couch/ - apt update - apt-get install -y openssh-server - echo "root:Docker!" | chpasswd - mkdir -p /tmp - chmod +x /tmp/ssh_setup.sh \ - && (sleep 1;/tmp/ssh_setup.sh 2>&1 > /dev/null) - cp /etc/sshd_config /etc/ssh/sshd_config - /etc/init.d/ssh restart - 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 c7b90dbdc4..e9ff6c6596 100644 --- a/hosting/single/Dockerfile +++ b/hosting/single/Dockerfile @@ -1,44 +1,59 @@ FROM node:18-slim as build # install node-gyp dependencies -RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends apt-utils cron g++ make python3 +RUN apt-get update && apt-get install -y --no-install-recommends g++ make python3 jq -# add pin script -WORKDIR / -ADD scripts/cleanup.sh ./ -RUN chmod +x /cleanup.sh -# build server +# copy and install dependencies WORKDIR /app -ADD packages/server . +COPY package.json . COPY yarn.lock . -RUN yarn install --production=true --network-timeout 1000000 -RUN /cleanup.sh +COPY lerna.json . +COPY .yarnrc . -# build worker -WORKDIR /worker -ADD packages/worker . -COPY yarn.lock . -RUN yarn install --production=true --network-timeout 1000000 -RUN /cleanup.sh +COPY packages/server/package.json packages/server/package.json +COPY packages/worker/package.json packages/worker/package.json +# string-templates does not get bundled during the esbuild process, so we want to use the local version +COPY packages/string-templates/package.json packages/string-templates/package.json -FROM budibase/couchdb + +COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.sh +RUN chmod +x ./scripts/removeWorkspaceDependencies.sh +RUN ./scripts/removeWorkspaceDependencies.sh packages/server/package.json +RUN ./scripts/removeWorkspaceDependencies.sh packages/worker/package.json + + +# We will never want to sync pro, but the script is still required +RUN echo '' > scripts/syncProPackage.js +RUN jq 'del(.scripts.postinstall)' package.json > temp.json && mv temp.json package.json +RUN ./scripts/removeWorkspaceDependencies.sh package.json +RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production + +# copy the actual code +COPY packages/server/dist packages/server/dist +COPY packages/server/pm2.config.js packages/server/pm2.config.js +COPY packages/server/client packages/server/client +COPY packages/server/builder packages/server/builder +COPY packages/worker/dist packages/worker/dist +COPY packages/worker/pm2.config.js packages/worker/pm2.config.js +COPY packages/string-templates packages/string-templates + + +FROM budibase/couchdb as runner ARG TARGETARCH ENV TARGETARCH $TARGETARCH +ENV NODE_MAJOR 18 #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 ENV TARGETBUILD $TARGETBUILD -COPY --from=build /app /app -COPY --from=build /worker /worker - # install base dependencies RUN apt-get update && \ - apt-get install -y --no-install-recommends software-properties-common nginx uuid-runtime redis-server + apt-get install -y --no-install-recommends software-properties-common nginx uuid-runtime redis-server libaio1 # Install postgres client for pg_dump utils -RUN apt install software-properties-common apt-transport-https gpg -y \ +RUN apt install -y software-properties-common apt-transport-https ca-certificates gnupg \ && curl -fsSl https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null \ && echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main | tee /etc/apt/sources.list.d/postgresql.list \ && apt update -y \ @@ -47,14 +62,12 @@ RUN apt install software-properties-common apt-transport-https gpg -y \ # install other dependencies, nodejs, oracle requirements, jdk8, redis, nginx WORKDIR /nodejs -RUN curl -sL https://deb.nodesource.com/setup_16.x -o /tmp/nodesource_setup.sh && \ - bash /tmp/nodesource_setup.sh && \ - apt-get install -y --no-install-recommends libaio1 nodejs && \ - npm install --global yarn pm2 +COPY scripts/install-node.sh ./install.sh +RUN chmod +x install.sh && ./install.sh # setup nginx -ADD hosting/single/nginx/nginx.conf /etc/nginx -ADD hosting/single/nginx/nginx-default-site.conf /etc/nginx/sites-enabled/default +COPY hosting/single/nginx/nginx.conf /etc/nginx +COPY hosting/single/nginx/nginx-default-site.conf /etc/nginx/sites-enabled/default RUN mkdir -p /var/log/nginx && \ touch /var/log/nginx/error.log && \ touch /var/run/nginx.pid && \ @@ -62,29 +75,39 @@ RUN mkdir -p /var/log/nginx && \ WORKDIR / RUN mkdir -p scripts/integrations/oracle -ADD packages/server/scripts/integrations/oracle scripts/integrations/oracle +COPY packages/server/scripts/integrations/oracle scripts/integrations/oracle RUN /bin/bash -e ./scripts/integrations/oracle/instantclient/linux/install.sh # setup minio WORKDIR /minio -ADD scripts/install-minio.sh ./install.sh +COPY scripts/install-minio.sh ./install.sh RUN chmod +x install.sh && ./install.sh # setup runner file WORKDIR / -ADD hosting/single/runner.sh . +COPY hosting/single/runner.sh . RUN chmod +x ./runner.sh -ADD hosting/single/healthcheck.sh . +COPY hosting/single/healthcheck.sh . RUN chmod +x ./healthcheck.sh # Script below sets the path for storing data based on $DATA_DIR # For Azure App Service install SSH & point data locations to /home -ADD hosting/single/ssh/sshd_config /etc/ -ADD hosting/single/ssh/ssh_setup.sh /tmp -RUN /build-target-paths.sh +COPY hosting/single/ssh/sshd_config /etc/ +COPY hosting/single/ssh/ssh_setup.sh /tmp + +# setup letsencrypt certificate +RUN apt-get install -y certbot python3-certbot-nginx +COPY hosting/letsencrypt /app/letsencrypt +RUN chmod +x /app/letsencrypt/certificate-request.sh /app/letsencrypt/certificate-renew.sh + +COPY --from=build /app/node_modules /node_modules +COPY --from=build /app/package.json /package.json +COPY --from=build /app/packages/server /app +COPY --from=build /app/packages/worker /worker +COPY --from=build /app/packages/string-templates /string-templates + +RUN cd /string-templates && yarn link && cd ../app && yarn link @budibase/string-templates && cd ../worker && yarn link @budibase/string-templates -# cleanup cache -RUN yarn cache clean -f EXPOSE 80 EXPOSE 443 @@ -92,20 +115,10 @@ EXPOSE 443 EXPOSE 2222 VOLUME /data -# setup letsencrypt certificate -RUN apt-get install -y certbot python3-certbot-nginx -ADD hosting/letsencrypt /app/letsencrypt -RUN chmod +x /app/letsencrypt/certificate-request.sh /app/letsencrypt/certificate-renew.sh -# Remove cached files -RUN rm -rf \ - /root/.cache \ - /root/.npm \ - /root/.pip \ - /usr/local/share/doc \ - /usr/share/doc \ - /usr/share/man \ - /var/lib/apt/lists/* \ - /tmp/* +ARG BUDIBASE_VERSION +# Ensuring the version argument is sent +RUN test -n "$BUDIBASE_VERSION" +ENV BUDIBASE_VERSION=$BUDIBASE_VERSION HEALTHCHECK --interval=15s --timeout=15s --start-period=45s CMD "/healthcheck.sh" diff --git a/hosting/single/Dockerfile.v2 b/hosting/single/Dockerfile.v2 deleted file mode 100644 index ec03a1b5a2..0000000000 --- a/hosting/single/Dockerfile.v2 +++ /dev/null @@ -1,131 +0,0 @@ -FROM node:18-slim as build - -# install node-gyp dependencies -RUN apt-get update && apt-get install -y --no-install-recommends g++ make python3 jq - - -# copy and install dependencies -WORKDIR /app -COPY package.json . -COPY yarn.lock . -COPY lerna.json . -COPY .yarnrc . - -COPY packages/server/package.json packages/server/package.json -COPY packages/worker/package.json packages/worker/package.json -# string-templates does not get bundled during the esbuild process, so we want to use the local version -COPY packages/string-templates/package.json packages/string-templates/package.json - - -COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.sh -RUN chmod +x ./scripts/removeWorkspaceDependencies.sh -RUN ./scripts/removeWorkspaceDependencies.sh packages/server/package.json -RUN ./scripts/removeWorkspaceDependencies.sh packages/worker/package.json - - -# We will never want to sync pro, but the script is still required -RUN echo '' > scripts/syncProPackage.js -RUN jq 'del(.scripts.postinstall)' package.json > temp.json && mv temp.json package.json -RUN ./scripts/removeWorkspaceDependencies.sh package.json -RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production - -# copy the actual code -COPY packages/server/dist packages/server/dist -COPY packages/server/pm2.config.js packages/server/pm2.config.js -COPY packages/server/client packages/server/client -COPY packages/server/builder packages/server/builder -COPY packages/worker/dist packages/worker/dist -COPY packages/worker/pm2.config.js packages/worker/pm2.config.js -COPY packages/string-templates packages/string-templates - - -FROM budibase/couchdb as runner -ARG TARGETARCH -ENV TARGETARCH $TARGETARCH -ENV NODE_MAJOR 18 -#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 -ENV TARGETBUILD $TARGETBUILD - -# install base dependencies -RUN apt-get update && \ - apt-get install -y --no-install-recommends software-properties-common nginx uuid-runtime redis-server libaio1 - -# Install postgres client for pg_dump utils -RUN apt install -y software-properties-common apt-transport-https ca-certificates gnupg \ - && curl -fsSl https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null \ - && echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main | tee /etc/apt/sources.list.d/postgresql.list \ - && apt update -y \ - && apt install postgresql-client-15 -y \ - && apt remove software-properties-common apt-transport-https gpg -y - -# install other dependencies, nodejs, oracle requirements, jdk8, redis, nginx -WORKDIR /nodejs -COPY scripts/install-node.sh ./install.sh -RUN chmod +x install.sh && ./install.sh - -# setup nginx -COPY hosting/single/nginx/nginx.conf /etc/nginx -COPY hosting/single/nginx/nginx-default-site.conf /etc/nginx/sites-enabled/default -RUN mkdir -p /var/log/nginx && \ - touch /var/log/nginx/error.log && \ - touch /var/run/nginx.pid && \ - usermod -a -G tty www-data - -WORKDIR / -RUN mkdir -p scripts/integrations/oracle -COPY packages/server/scripts/integrations/oracle scripts/integrations/oracle -RUN /bin/bash -e ./scripts/integrations/oracle/instantclient/linux/install.sh - -# setup minio -WORKDIR /minio -COPY scripts/install-minio.sh ./install.sh -RUN chmod +x install.sh && ./install.sh - -# setup runner file -WORKDIR / -COPY hosting/single/runner.sh . -RUN chmod +x ./runner.sh -COPY hosting/single/healthcheck.sh . -RUN chmod +x ./healthcheck.sh - -# Script below sets the path for storing data based on $DATA_DIR -# For Azure App Service install SSH & point data locations to /home -COPY hosting/single/ssh/sshd_config /etc/ -COPY hosting/single/ssh/ssh_setup.sh /tmp -RUN /build-target-paths.sh - - -# setup letsencrypt certificate -RUN apt-get install -y certbot python3-certbot-nginx -COPY hosting/letsencrypt /app/letsencrypt -RUN chmod +x /app/letsencrypt/certificate-request.sh /app/letsencrypt/certificate-renew.sh - -COPY --from=build /app/node_modules /node_modules -COPY --from=build /app/package.json /package.json -COPY --from=build /app/packages/server /app -COPY --from=build /app/packages/worker /worker -COPY --from=build /app/packages/string-templates /string-templates - -RUN cd /string-templates && yarn link && cd ../app && yarn link @budibase/string-templates && cd ../worker && yarn link @budibase/string-templates - - -EXPOSE 80 -EXPOSE 443 -# Expose port 2222 for SSH on Azure App Service build -EXPOSE 2222 -VOLUME /data - -ARG BUDIBASE_VERSION -# Ensuring the version argument is sent -RUN test -n "$BUDIBASE_VERSION" -ENV BUDIBASE_VERSION=$BUDIBASE_VERSION - -HEALTHCHECK --interval=15s --timeout=15s --start-period=45s CMD "/healthcheck.sh" - -# must set this just before running -ENV NODE_ENV=production -WORKDIR / - -CMD ["./runner.sh"] diff --git a/hosting/single/healthcheck.sh b/hosting/single/healthcheck.sh index 592b3e94fa..12e340062c 100644 --- a/hosting/single/healthcheck.sh +++ b/hosting/single/healthcheck.sh @@ -25,7 +25,7 @@ if [[ $(curl -s -w "%{http_code}\n" http://localhost:4002/health -o /dev/null) - healthy=false fi -if [[ $(curl -s -w "%{http_code}\n" http://localhost:5984/ -o /dev/null) -ne 200 ]]; then +if [[ $(curl -s -w "%{http_code}\n" http://localhost:5984/_up -o /dev/null) -ne 200 ]]; then echo 'ERROR: CouchDB is not running'; healthy=false fi diff --git a/hosting/single/runner.sh b/hosting/single/runner.sh index 87201c95c0..f4b2b5b127 100644 --- a/hosting/single/runner.sh +++ b/hosting/single/runner.sh @@ -22,11 +22,11 @@ declare -a DOCKER_VARS=("APP_PORT" "APPS_URL" "ARCHITECTURE" "BUDIBASE_ENVIRONME # Azure App Service customisations if [[ "${TARGETBUILD}" = "aas" ]]; then - DATA_DIR="${DATA_DIR:-/home}" + export DATA_DIR="${DATA_DIR:-/home}" WEBSITES_ENABLE_APP_SERVICE_STORAGE=true /etc/init.d/ssh start else - DATA_DIR=${DATA_DIR:-/data} + export DATA_DIR=${DATA_DIR:-/data} fi mkdir -p ${DATA_DIR} # Mount NFS or GCP Filestore if env vars exist for it diff --git a/lerna.json b/lerna.json index 9b22d286b5..40e167f36c 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.13.12", + "version": "2.13.17", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/src/auth/auth.ts b/packages/backend-core/src/auth/auth.ts index 0100a2d0e2..e31bc81eed 100644 --- a/packages/backend-core/src/auth/auth.ts +++ b/packages/backend-core/src/auth/auth.ts @@ -1,5 +1,6 @@ const _passport = require("koa-passport") const LocalStrategy = require("passport-local").Strategy + import { getGlobalDB } from "../context" import { Cookie } from "../constants" import { getSessionsForUser, invalidateSessions } from "../security/sessions" @@ -26,6 +27,7 @@ import { clearCookie, getCookie } from "../utils" import { ssoSaveUserNoOp } from "../middleware/passport/sso/sso" const refresh = require("passport-oauth2-refresh") + export { auditLog, authError, diff --git a/packages/backend-core/src/configs/configs.ts b/packages/backend-core/src/configs/configs.ts index 0c83ed005d..0d189e3f7d 100644 --- a/packages/backend-core/src/configs/configs.ts +++ b/packages/backend-core/src/configs/configs.ts @@ -17,7 +17,6 @@ import { DocumentType, SEPARATOR } from "../constants" import { CacheKey, TTL, withCache } from "../cache" import * as context from "../context" import env from "../environment" -import environment from "../environment" // UTILS @@ -181,10 +180,10 @@ export async function getGoogleDatasourceConfig(): Promise< } export function getDefaultGoogleConfig(): GoogleInnerConfig | undefined { - if (environment.GOOGLE_CLIENT_ID && environment.GOOGLE_CLIENT_SECRET) { + if (env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) { return { - clientID: environment.GOOGLE_CLIENT_ID!, - clientSecret: environment.GOOGLE_CLIENT_SECRET!, + clientID: env.GOOGLE_CLIENT_ID!, + clientSecret: env.GOOGLE_CLIENT_SECRET!, activated: true, } } diff --git a/packages/backend-core/src/constants/db.ts b/packages/backend-core/src/constants/db.ts index bb944556af..ac00483021 100644 --- a/packages/backend-core/src/constants/db.ts +++ b/packages/backend-core/src/constants/db.ts @@ -1,4 +1,5 @@ import { prefixed, DocumentType } from "@budibase/types" + export { SEPARATOR, UNICODE_MAX, diff --git a/packages/backend-core/src/db/utils.ts b/packages/backend-core/src/db/utils.ts index d7a4b8224a..906a95e1db 100644 --- a/packages/backend-core/src/db/utils.ts +++ b/packages/backend-core/src/db/utils.ts @@ -6,6 +6,7 @@ import { AppState, DeletedApp, getAppMetadata } from "../cache/appMetadata" import { isDevApp, isDevAppID, getProdAppID } from "../docIds/conversions" import { App, Database } from "@budibase/types" import { getStartEndKeyURL } from "../docIds" + export * from "../docIds" /** diff --git a/packages/backend-core/src/docIds/conversions.ts b/packages/backend-core/src/docIds/conversions.ts index b168b74e16..ec43d01389 100644 --- a/packages/backend-core/src/docIds/conversions.ts +++ b/packages/backend-core/src/docIds/conversions.ts @@ -1,5 +1,6 @@ import { APP_DEV_PREFIX, APP_PREFIX } from "../constants" import { App } from "@budibase/types" + const NO_APP_ERROR = "No app provided" export function isDevAppID(appId?: string) { diff --git a/packages/backend-core/src/events/processors/posthog/index.ts b/packages/backend-core/src/events/processors/posthog/index.ts index dceb10d2cd..5a2b1afc9f 100644 --- a/packages/backend-core/src/events/processors/posthog/index.ts +++ b/packages/backend-core/src/events/processors/posthog/index.ts @@ -1,2 +1,3 @@ import PosthogProcessor from "./PosthogProcessor" + export default PosthogProcessor diff --git a/packages/backend-core/src/events/processors/posthog/tests/PosthogProcessor.spec.ts b/packages/backend-core/src/events/processors/posthog/tests/PosthogProcessor.spec.ts index 0722fc3293..d9a5504073 100644 --- a/packages/backend-core/src/events/processors/posthog/tests/PosthogProcessor.spec.ts +++ b/packages/backend-core/src/events/processors/posthog/tests/PosthogProcessor.spec.ts @@ -1,7 +1,9 @@ import { testEnv } from "../../../../../tests/extra" import PosthogProcessor from "../PosthogProcessor" import { Event, IdentityType, Hosting } from "@budibase/types" + const tk = require("timekeeper") + import * as cache from "../../../../cache/generic" import { CacheKey } from "../../../../cache/generic" import * as context from "../../../../context" diff --git a/packages/backend-core/src/features/index.ts b/packages/backend-core/src/features/index.ts index 8f5c903e05..ad517082de 100644 --- a/packages/backend-core/src/features/index.ts +++ b/packages/backend-core/src/features/index.ts @@ -1,5 +1,6 @@ import env from "../environment" import * as context from "../context" + export * from "./installation" /** diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts index 2cfd517941..d04f48e5fc 100644 --- a/packages/backend-core/src/index.ts +++ b/packages/backend-core/src/index.ts @@ -38,6 +38,7 @@ export * as docIds from "./docIds" // circular dependencies import * as context from "./context" import * as _tenancy from "./tenancy" + export const tenancy = { ..._tenancy, ...context, diff --git a/packages/backend-core/src/installation.ts b/packages/backend-core/src/installation.ts index 17eda2004d..ca35b926fb 100644 --- a/packages/backend-core/src/installation.ts +++ b/packages/backend-core/src/installation.ts @@ -1,7 +1,6 @@ import { newid } from "./utils" import * as events from "./events" -import { StaticDatabases } from "./db" -import { doWithDB } from "./db" +import { StaticDatabases, doWithDB } from "./db" import { Installation, IdentityType, Database } from "@budibase/types" import * as context from "./context" import semver from "semver" diff --git a/packages/backend-core/src/logging/correlation/correlation.ts b/packages/backend-core/src/logging/correlation/correlation.ts index b5b47df9c6..0498bd43d5 100644 --- a/packages/backend-core/src/logging/correlation/correlation.ts +++ b/packages/backend-core/src/logging/correlation/correlation.ts @@ -1,4 +1,5 @@ import { Header } from "../../constants" + const correlator = require("correlation-id") export const setHeader = (headers: any) => { diff --git a/packages/backend-core/src/logging/correlation/middleware.ts b/packages/backend-core/src/logging/correlation/middleware.ts index f77714a5ae..45baee1fb1 100644 --- a/packages/backend-core/src/logging/correlation/middleware.ts +++ b/packages/backend-core/src/logging/correlation/middleware.ts @@ -1,5 +1,6 @@ import { Header } from "../../constants" import { v4 as uuid } from "uuid" + const correlator = require("correlation-id") const correlation = (ctx: any, next: any) => { diff --git a/packages/backend-core/src/logging/pino/middleware.ts b/packages/backend-core/src/logging/pino/middleware.ts index 569420c5f2..df18a35eb1 100644 --- a/packages/backend-core/src/logging/pino/middleware.ts +++ b/packages/backend-core/src/logging/pino/middleware.ts @@ -1,9 +1,12 @@ import env from "../../environment" import { logger } from "./logger" import { IncomingMessage } from "http" + const pino = require("koa-pino-logger") + import { Options } from "pino-http" import { Ctx } from "@budibase/types" + const correlator = require("correlation-id") export function pinoSettings(): Options { diff --git a/packages/backend-core/src/middleware/index.ts b/packages/backend-core/src/middleware/index.ts index 980bf06b00..e1eb7f1d26 100644 --- a/packages/backend-core/src/middleware/index.ts +++ b/packages/backend-core/src/middleware/index.ts @@ -2,6 +2,7 @@ export * as local from "./passport/local" export * as google from "./passport/sso/google" export * as oidc from "./passport/sso/oidc" import * as datasourceGoogle from "./passport/datasource/google" + export const datasource = { google: datasourceGoogle, } diff --git a/packages/backend-core/src/middleware/passport/sso/google.ts b/packages/backend-core/src/middleware/passport/sso/google.ts index ad7593e63d..2a08ad7665 100644 --- a/packages/backend-core/src/middleware/passport/sso/google.ts +++ b/packages/backend-core/src/middleware/passport/sso/google.ts @@ -8,6 +8,7 @@ import { SaveSSOUserFunction, GoogleInnerConfig, } from "@budibase/types" + const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy export function buildVerifyFn(saveUserFn: SaveSSOUserFunction) { diff --git a/packages/backend-core/src/middleware/passport/sso/tests/google.spec.ts b/packages/backend-core/src/middleware/passport/sso/tests/google.spec.ts index d0689a1f0a..9bf855b3c5 100644 --- a/packages/backend-core/src/middleware/passport/sso/tests/google.spec.ts +++ b/packages/backend-core/src/middleware/passport/sso/tests/google.spec.ts @@ -6,6 +6,7 @@ const mockStrategy = require("passport-google-oauth").OAuth2Strategy jest.mock("../sso") import * as _sso from "../sso" + const sso = jest.mocked(_sso) const mockSaveUserFn = jest.fn() diff --git a/packages/backend-core/src/middleware/passport/sso/tests/sso.spec.ts b/packages/backend-core/src/middleware/passport/sso/tests/sso.spec.ts index c3ddf220e6..d3486a5b14 100644 --- a/packages/backend-core/src/middleware/passport/sso/tests/sso.spec.ts +++ b/packages/backend-core/src/middleware/passport/sso/tests/sso.spec.ts @@ -11,6 +11,7 @@ const mockSaveUser = jest.fn() jest.mock("../../../../users") import * as _users from "../../../../users" + const users = jest.mocked(_users) const getErrorMessage = () => { diff --git a/packages/backend-core/src/middleware/tests/builder.spec.ts b/packages/backend-core/src/middleware/tests/builder.spec.ts index d350eff4f6..0514dc13f0 100644 --- a/packages/backend-core/src/middleware/tests/builder.spec.ts +++ b/packages/backend-core/src/middleware/tests/builder.spec.ts @@ -5,6 +5,7 @@ import { structures } from "../../../tests" import { ContextUser, ServiceType } from "@budibase/types" import { doInAppContext } from "../../context" import env from "../../environment" + env._set("SERVICE_TYPE", ServiceType.APPS) const appId = "app_aaa" diff --git a/packages/backend-core/src/objectStore/objectStore.ts b/packages/backend-core/src/objectStore/objectStore.ts index 76d2dd6689..cdaf19fa55 100644 --- a/packages/backend-core/src/objectStore/objectStore.ts +++ b/packages/backend-core/src/objectStore/objectStore.ts @@ -1,4 +1,5 @@ const sanitize = require("sanitize-s3-objectkey") + import AWS from "aws-sdk" import stream, { Readable } from "stream" import fetch from "node-fetch" diff --git a/packages/backend-core/src/redis/redlockImpl.ts b/packages/backend-core/src/redis/redlockImpl.ts index 7fe61a409e..266f1fe989 100644 --- a/packages/backend-core/src/redis/redlockImpl.ts +++ b/packages/backend-core/src/redis/redlockImpl.ts @@ -3,6 +3,7 @@ import { getLockClient } from "./init" import { LockOptions, LockType } from "@budibase/types" import * as context from "../context" import env from "../environment" +import { logWarn } from "../logging" async function getClient( type: LockType, @@ -116,7 +117,7 @@ export async function doWithLock( const result = await task() return { executed: true, result } } catch (e: any) { - console.warn("lock error") + logWarn(`lock type: ${opts.type} error`, e) // lock limit exceeded if (e.name === "LockError") { if (opts.type === LockType.TRY_ONCE) { @@ -124,11 +125,9 @@ export async function doWithLock( // due to retry count (0) exceeded return { executed: false } } else { - console.error(e) throw e } } else { - console.error(e) throw e } } finally { diff --git a/packages/backend-core/src/security/permissions.ts b/packages/backend-core/src/security/permissions.ts index fe4095d210..98704f16c6 100644 --- a/packages/backend-core/src/security/permissions.ts +++ b/packages/backend-core/src/security/permissions.ts @@ -160,4 +160,5 @@ export function isPermissionLevelHigherThanRead(level: PermissionLevel) { // utility as a lot of things need simply the builder permission export const BUILDER = PermissionType.BUILDER +export const CREATOR = PermissionType.CREATOR export const GLOBAL_BUILDER = PermissionType.GLOBAL_BUILDER diff --git a/packages/backend-core/src/security/roles.ts b/packages/backend-core/src/security/roles.ts index 0d33031de5..4f048c0a11 100644 --- a/packages/backend-core/src/security/roles.ts +++ b/packages/backend-core/src/security/roles.ts @@ -1,7 +1,12 @@ import { BuiltinPermissionID, PermissionLevel } from "./permissions" -import { prefixRoleID, getRoleParams, DocumentType, SEPARATOR } from "../db" +import { + prefixRoleID, + getRoleParams, + DocumentType, + SEPARATOR, + doWithDB, +} from "../db" import { getAppDB } from "../context" -import { doWithDB } from "../db" import { Screen, Role as RoleDoc } from "@budibase/types" import cloneDeep from "lodash/fp/cloneDeep" diff --git a/packages/backend-core/src/security/sessions.ts b/packages/backend-core/src/security/sessions.ts index 5a535c0c46..a86a829b17 100644 --- a/packages/backend-core/src/security/sessions.ts +++ b/packages/backend-core/src/security/sessions.ts @@ -1,6 +1,7 @@ const redis = require("../redis/init") const { v4: uuidv4 } = require("uuid") const { logWarn } = require("../logging") + import env from "../environment" import { Session, diff --git a/packages/backend-core/src/users/db.ts b/packages/backend-core/src/users/db.ts index bd85097bbd..326bed3cc5 100644 --- a/packages/backend-core/src/users/db.ts +++ b/packages/backend-core/src/users/db.ts @@ -1,9 +1,8 @@ import env from "../environment" import * as eventHelpers from "./events" -import * as accounts from "../accounts" import * as accountSdk from "../accounts" import * as cache from "../cache" -import { getGlobalDB, getIdentity, getTenantId } from "../context" +import { doInTenant, getGlobalDB, getIdentity, getTenantId } from "../context" import * as dbUtils from "../db" import { EmailUnavailableError, HTTPError } from "../errors" import * as platform from "../platform" @@ -11,12 +10,10 @@ import * as sessions from "../security/sessions" import * as usersCore from "./users" import { Account, - AllDocsResponse, BulkUserCreated, BulkUserDeleted, isSSOAccount, isSSOUser, - RowResponse, SaveUserOpts, User, UserStatus, @@ -149,12 +146,12 @@ export class UserDB { static async allUsers() { const db = getGlobalDB() - const response = await db.allDocs( + const response = await db.allDocs( dbUtils.getGlobalUserParams(null, { include_docs: true, }) ) - return response.rows.map((row: any) => row.doc) + return response.rows.map(row => row.doc!) } static async countUsersByApp(appId: string) { @@ -212,13 +209,6 @@ export class UserDB { throw new Error("_id or email is required") } - if ( - user.builder?.apps?.length && - !(await UserDB.features.isAppBuildersEnabled()) - ) { - throw new Error("Unable to update app builders, please check license") - } - let dbUser: User | undefined if (_id) { // try to get existing user from db @@ -467,7 +457,7 @@ export class UserDB { if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) { // root account holder can't be deleted from inside budibase const email = dbUser.email - const account = await accounts.getAccount(email) + const account = await accountSdk.getAccount(email) if (account) { if (dbUser.userId === getIdentity()!._id) { throw new HTTPError('Please visit "Account" to delete this user', 400) @@ -488,6 +478,37 @@ export class UserDB { await sessions.invalidateSessions(userId, { reason: "deletion" }) } + static async createAdminUser( + email: string, + password: string, + tenantId: string, + opts?: { ssoId?: string; hashPassword?: boolean; requirePassword?: boolean } + ) { + const user: User = { + email: email, + password: password, + createdAt: Date.now(), + roles: {}, + builder: { + global: true, + }, + admin: { + global: true, + }, + tenantId, + } + if (opts?.ssoId) { + user.ssoId = opts.ssoId + } + // always bust checklist beforehand, if an error occurs but can proceed, don't get + // stuck in a cycle + await cache.bustCache(cache.CacheKey.CHECKLIST) + return await UserDB.save(user, { + hashPassword: opts?.hashPassword, + requirePassword: opts?.requirePassword, + }) + } + static async getGroups(groupIds: string[]) { return await this.groups.getBulk(groupIds) } diff --git a/packages/backend-core/src/users/users.ts b/packages/backend-core/src/users/users.ts index 9f4a41f6df..cc2b4fc27f 100644 --- a/packages/backend-core/src/users/users.ts +++ b/packages/backend-core/src/users/users.ts @@ -25,6 +25,7 @@ import { import { getGlobalDB } from "../context" import * as context from "../context" import { isCreator } from "./utils" +import { UserDB } from "./db" type GetOpts = { cleanup?: boolean } @@ -43,7 +44,7 @@ function removeUserPassword(users: User | User[]) { return users } -export const isSupportedUserSearch = (query: SearchQuery) => { +export function isSupportedUserSearch(query: SearchQuery) { const allowed = [ { op: SearchQueryOperators.STRING, key: "email" }, { op: SearchQueryOperators.EQUAL, key: "_id" }, @@ -68,10 +69,10 @@ export const isSupportedUserSearch = (query: SearchQuery) => { return true } -export const bulkGetGlobalUsersById = async ( +export async function bulkGetGlobalUsersById( userIds: string[], opts?: GetOpts -) => { +) { const db = getGlobalDB() let users = ( await db.allDocs({ @@ -85,7 +86,7 @@ export const bulkGetGlobalUsersById = async ( return users } -export const getAllUserIds = async () => { +export async function getAllUserIds() { const db = getGlobalDB() const startKey = `${DocumentType.USER}${SEPARATOR}` const response = await db.allDocs({ @@ -95,7 +96,7 @@ export const getAllUserIds = async () => { return response.rows.map(row => row.id) } -export const bulkUpdateGlobalUsers = async (users: User[]) => { +export async function bulkUpdateGlobalUsers(users: User[]) { const db = getGlobalDB() return (await db.bulkDocs(users)) as BulkDocsResponse } @@ -113,10 +114,10 @@ export async function getById(id: string, opts?: GetOpts): Promise { * Given an email address this will use a view to search through * all the users to find one with this email address. */ -export const getGlobalUserByEmail = async ( +export async function getGlobalUserByEmail( email: String, opts?: GetOpts -): Promise => { +): Promise { if (email == null) { throw "Must supply an email address to view" } @@ -139,11 +140,23 @@ export const getGlobalUserByEmail = async ( return user } -export const searchGlobalUsersByApp = async ( +export async function doesUserExist(email: string) { + try { + const user = await getGlobalUserByEmail(email) + if (Array.isArray(user) || user != null) { + return true + } + } catch (err) { + return false + } + return false +} + +export async function searchGlobalUsersByApp( appId: any, opts: DatabaseQueryOpts, getOpts?: GetOpts -) => { +) { if (typeof appId !== "string") { throw new Error("Must provide a string based app ID") } @@ -167,10 +180,10 @@ export const searchGlobalUsersByApp = async ( Return any user who potentially has access to the application Admins, developers and app users with the explicitly role. */ -export const searchGlobalUsersByAppAccess = async ( +export async function searchGlobalUsersByAppAccess( appId: any, opts?: { limit?: number } -) => { +) { const roleSelector = `roles.${appId}` let orQuery: any[] = [ @@ -205,7 +218,7 @@ export const searchGlobalUsersByAppAccess = async ( return resp.rows } -export const getGlobalUserByAppPage = (appId: string, user: User) => { +export function getGlobalUserByAppPage(appId: string, user: User) { if (!user) { return } @@ -215,11 +228,11 @@ export const getGlobalUserByAppPage = (appId: string, user: User) => { /** * Performs a starts with search on the global email view. */ -export const searchGlobalUsersByEmail = async ( +export async function searchGlobalUsersByEmail( email: string | unknown, opts: any, getOpts?: GetOpts -) => { +) { if (typeof email !== "string") { throw new Error("Must provide a string to search by") } @@ -242,12 +255,12 @@ export const searchGlobalUsersByEmail = async ( } const PAGE_LIMIT = 8 -export const paginatedUsers = async ({ +export async function paginatedUsers({ bookmark, query, appId, limit, -}: SearchUsersRequest = {}) => { +}: SearchUsersRequest = {}) { const db = getGlobalDB() const pageSize = limit ?? PAGE_LIMIT const pageLimit = pageSize + 1 @@ -324,3 +337,20 @@ export function cleanseUserObject(user: User | ContextUser, base?: User) { } return user } + +export async function addAppBuilder(user: User, appId: string) { + const prodAppId = getProdAppID(appId) + user.builder ??= {} + user.builder.creator = true + user.builder.apps ??= [] + user.builder.apps.push(prodAppId) + await UserDB.save(user, { hashPassword: false }) +} + +export async function removeAppBuilder(user: User, appId: string) { + const prodAppId = getProdAppID(appId) + if (user.builder && user.builder.apps?.includes(prodAppId)) { + user.builder.apps = user.builder.apps.filter(id => id !== prodAppId) + } + await UserDB.save(user, { hashPassword: false }) +} diff --git a/packages/backend-core/src/utils/hashing.ts b/packages/backend-core/src/utils/hashing.ts index aba11f38e6..54d7de4aba 100644 --- a/packages/backend-core/src/utils/hashing.ts +++ b/packages/backend-core/src/utils/hashing.ts @@ -1,4 +1,5 @@ import env from "../environment" + export * from "../docIds/newid" const bcrypt = env.JS_BCRYPT ? require("bcryptjs") : require("bcrypt") diff --git a/packages/backend-core/src/utils/utils.ts b/packages/backend-core/src/utils/utils.ts index 1c1ca8473b..b10d9ebdc0 100644 --- a/packages/backend-core/src/utils/utils.ts +++ b/packages/backend-core/src/utils/utils.ts @@ -11,6 +11,7 @@ import { TenantResolutionStrategy, } from "@budibase/types" import type { SetOption } from "cookies" + const jwt = require("jsonwebtoken") const APP_PREFIX = DocumentType.APP + SEPARATOR diff --git a/packages/backend-core/tests/core/utilities/mocks/alerts.ts b/packages/backend-core/tests/core/utilities/mocks/alerts.ts index 90c9759c92..0b26e98363 100644 --- a/packages/backend-core/tests/core/utilities/mocks/alerts.ts +++ b/packages/backend-core/tests/core/utilities/mocks/alerts.ts @@ -1,3 +1,4 @@ jest.mock("../../../../src/logging/alerts") import * as _alerts from "../../../../src/logging/alerts" + export const alerts = jest.mocked(_alerts) diff --git a/packages/backend-core/tests/core/utilities/mocks/index.ts b/packages/backend-core/tests/core/utilities/mocks/index.ts index 9a72b38ef5..8705e563cb 100644 --- a/packages/backend-core/tests/core/utilities/mocks/index.ts +++ b/packages/backend-core/tests/core/utilities/mocks/index.ts @@ -1,5 +1,6 @@ jest.mock("../../../../src/accounts") import * as _accounts from "../../../../src/accounts" + export const accounts = jest.mocked(_accounts) export * as date from "./date" diff --git a/packages/backend-core/tests/core/utilities/structures/generator.ts b/packages/backend-core/tests/core/utilities/structures/generator.ts index ed4dac8255..64eb5ecc97 100644 --- a/packages/backend-core/tests/core/utilities/structures/generator.ts +++ b/packages/backend-core/tests/core/utilities/structures/generator.ts @@ -1,2 +1,3 @@ import Chance from "./Chance" + export const generator = new Chance() diff --git a/packages/backend-core/tests/jestSetup.ts b/packages/backend-core/tests/jestSetup.ts index 42a24ce733..e5d144290b 100644 --- a/packages/backend-core/tests/jestSetup.ts +++ b/packages/backend-core/tests/jestSetup.ts @@ -9,6 +9,7 @@ mocks.fetch.enable() // mock all dates to 2020-01-01T00:00:00.000Z // use tk.reset() to use real dates in individual tests import tk from "timekeeper" + tk.freeze(mocks.date.MOCK_DATE) if (!process.env.DEBUG) { diff --git a/packages/bbui/src/ActionGroup/ActionGroup.svelte b/packages/bbui/src/ActionGroup/ActionGroup.svelte index 43d8cd8de5..978e920c42 100644 --- a/packages/bbui/src/ActionGroup/ActionGroup.svelte +++ b/packages/bbui/src/ActionGroup/ActionGroup.svelte @@ -1,5 +1,6 @@ diff --git a/packages/bbui/src/Modal/Content.svelte b/packages/bbui/src/Modal/Content.svelte index 550994ef7c..485229acd6 100644 --- a/packages/bbui/src/Modal/Content.svelte +++ b/packages/bbui/src/Modal/Content.svelte @@ -1,5 +1,6 @@ diff --git a/packages/bbui/src/OptionSelectDnD/OptionSelectDnD.svelte b/packages/bbui/src/OptionSelectDnD/OptionSelectDnD.svelte index 8b13135b33..d4ecda246d 100644 --- a/packages/bbui/src/OptionSelectDnD/OptionSelectDnD.svelte +++ b/packages/bbui/src/OptionSelectDnD/OptionSelectDnD.svelte @@ -4,6 +4,7 @@ import Icon from "../Icon/Icon.svelte" import Popover from "../Popover/Popover.svelte" import { onMount } from "svelte" + const flipDurationMs = 150 export let constraints diff --git a/packages/bbui/src/Popover/Popover.svelte b/packages/bbui/src/Popover/Popover.svelte index 4c4b818440..a68430e973 100644 --- a/packages/bbui/src/Popover/Popover.svelte +++ b/packages/bbui/src/Popover/Popover.svelte @@ -1,11 +1,10 @@ diff --git a/packages/bbui/src/Table/ArrayRenderer.svelte b/packages/bbui/src/Table/ArrayRenderer.svelte index 637454dbca..303397054a 100644 --- a/packages/bbui/src/Table/ArrayRenderer.svelte +++ b/packages/bbui/src/Table/ArrayRenderer.svelte @@ -1,6 +1,7 @@ {#if $database?._id}
- selectTable(TableNames.USERS)} - selectedBy={$userSelectedResourceMap[TableNames.USERS]} - /> - {#each enrichedDataSources as datasource} + {#if showAppUsersTable} + selectTable(TableNames.USERS)} + selectedBy={$userSelectedResourceMap[TableNames.USERS]} + /> + {/if} + {#each enrichedDataSources.filter(ds => ds.show) as datasource} {#if datasource.open} - - {#each $queries.list.filter(query => query.datasourceId === datasource._id) as query} + + {#each datasource.queries as query} +
+ There aren't any datasources matching that name +
+ + {/if}
{/if} @@ -240,4 +148,8 @@ place-items: center; flex: 0 0 24px; } + + .no-results { + color: var(--spectrum-global-color-gray-600); + } diff --git a/packages/builder/src/components/backend/DatasourceNavigator/datasourceUtils.js b/packages/builder/src/components/backend/DatasourceNavigator/datasourceUtils.js new file mode 100644 index 0000000000..bc7fa53b49 --- /dev/null +++ b/packages/builder/src/components/backend/DatasourceNavigator/datasourceUtils.js @@ -0,0 +1,181 @@ +import { TableNames } from "constants" + +const showDatasourceOpen = ({ + selected, + containsSelected, + dsToggledStatus, + searchTerm, + onlyOneSource, +}) => { + // We want to display all the ds expanded while filtering ds + if (searchTerm) { + return true + } + + // If the toggle status has been a value + if (dsToggledStatus !== undefined) { + return dsToggledStatus + } + + if (onlyOneSource) { + return true + } + + return selected || containsSelected +} + +const containsActiveEntity = ( + datasource, + params, + isActive, + tables, + queries, + views, + viewsV2 +) => { + // Check for being on a datasource page + if (params.datasourceId === datasource._id) { + return true + } + + // Check for hardcoded datasource edge cases + if ( + isActive("./datasource/bb_internal") && + datasource._id === "bb_internal" + ) { + return true + } + if ( + isActive("./datasource/datasource_internal_bb_default") && + datasource._id === "datasource_internal_bb_default" + ) { + return true + } + + // Check for a matching query + if (params.queryId) { + const query = queries.list?.find(q => q._id === params.queryId) + return datasource._id === query?.datasourceId + } + + // If there are no entities it can't contain anything + if (!datasource.entities) { + return false + } + + // Get a list of table options + let options = datasource.entities + if (!Array.isArray(options)) { + options = Object.values(options) + } + + // Check for a matching table + if (params.tableId) { + const selectedTable = tables.selected?._id + return options.find(x => x._id === selectedTable) != null + } + + // Check for a matching view + const selectedView = views.selected?.name + const viewTable = options.find(table => { + return table.views?.[selectedView] != null + }) + if (viewTable) { + return true + } + + // Check for a matching viewV2 + const viewV2Table = options.find(x => x._id === viewsV2.selected?.tableId) + return viewV2Table != null +} + +export const enrichDatasources = ( + datasources, + params, + isActive, + tables, + queries, + views, + viewsV2, + toggledDatasources, + searchTerm +) => { + if (!datasources?.list?.length) { + return [] + } + + const onlySource = datasources.list.length === 1 + return datasources.list.map(datasource => { + const selected = + isActive("./datasource") && + datasources.selectedDatasourceId === datasource._id + const containsSelected = containsActiveEntity( + datasource, + params, + isActive, + tables, + queries, + views, + viewsV2 + ) + + const dsTables = tables.list.filter( + table => + table.sourceId === datasource._id && table._id !== TableNames.USERS + ) + const dsQueries = queries.list.filter( + query => query.datasourceId === datasource._id + ) + + const open = showDatasourceOpen({ + selected, + containsSelected, + dsToggledStatus: toggledDatasources[datasource._id], + searchTerm, + onlyOneSource: onlySource, + }) + + const visibleDsQueries = dsQueries.filter( + q => + !searchTerm || + q.name?.toLowerCase()?.indexOf(searchTerm.toLowerCase()) > -1 + ) + + const visibleDsTables = dsTables + .map(t => ({ + ...t, + views: !searchTerm + ? t.views + : Object.keys(t.views || {}) + .filter( + viewName => + viewName.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1 + ) + .reduce( + (acc, viewName) => ({ ...acc, [viewName]: t.views[viewName] }), + {} + ), + })) + .filter( + table => + !searchTerm || + table.name?.toLowerCase()?.indexOf(searchTerm.toLowerCase()) > -1 || + Object.keys(table.views).length + ) + + const show = !!( + !searchTerm || + visibleDsQueries.length || + visibleDsTables.length + ) + return { + ...datasource, + selected, + containsSelected, + open, + queries: visibleDsQueries, + tables: visibleDsTables, + show, + } + }) +} diff --git a/packages/builder/src/components/backend/DatasourceNavigator/modals/UpdateDatasourceModal.svelte b/packages/builder/src/components/backend/DatasourceNavigator/modals/UpdateDatasourceModal.svelte index 991e170a2e..2bc374056d 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/modals/UpdateDatasourceModal.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/modals/UpdateDatasourceModal.svelte @@ -1,8 +1,7 @@ + + + +
+ + +
+ {title} +
+
+ +
+ +
+ +
+
+ + diff --git a/packages/builder/src/components/common/NavItem.svelte b/packages/builder/src/components/common/NavItem.svelte index 2c8a862535..1c9267ca18 100644 --- a/packages/builder/src/components/common/NavItem.svelte +++ b/packages/builder/src/components/common/NavItem.svelte @@ -189,6 +189,7 @@ flex: 0 0 20px; pointer-events: all; order: 0; + transition: transform 100ms linear; } .icon.arrow.absolute { position: absolute; diff --git a/packages/builder/src/components/common/RoleSelect.svelte b/packages/builder/src/components/common/RoleSelect.svelte index 2df61926e1..1158107fd6 100644 --- a/packages/builder/src/components/common/RoleSelect.svelte +++ b/packages/builder/src/components/common/RoleSelect.svelte @@ -20,73 +20,91 @@ export let allowedRoles = null export let allowCreator = false export let fancySelect = false + export let labelPrefix = null const dispatch = createEventDispatcher() const RemoveID = "remove" + $: enrichLabel = label => (labelPrefix ? `${labelPrefix} ${label}` : label) $: options = getOptions( $roles, allowPublic, allowRemove, allowedRoles, - allowCreator + allowCreator, + enrichLabel ) + const getOptions = ( roles, allowPublic, allowRemove, allowedRoles, - allowCreator + allowCreator, + enrichLabel ) => { + // Use roles whitelist if specified if (allowedRoles?.length) { - const filteredRoles = roles.filter(role => - allowedRoles.includes(role._id) - ) - return [ - ...filteredRoles, - ...(allowedRoles.includes(Constants.Roles.CREATOR) - ? [{ _id: Constants.Roles.CREATOR, name: "Creator", enabled: false }] - : []), - ] - } - let newRoles = [...roles] - - if (allowCreator) { - newRoles = [ - { + let options = roles + .filter(role => allowedRoles.includes(role._id)) + .map(role => ({ + name: enrichLabel(role.name), + _id: role._id, + })) + if (allowedRoles.includes(Constants.Roles.CREATOR)) { + options.push({ _id: Constants.Roles.CREATOR, - name: "Creator", - tag: - !$licensing.perAppBuildersEnabled && - capitalise(Constants.PlanType.BUSINESS), - }, - ...newRoles, - ] + name: "Can edit", + enabled: false, + }) + } + return options } + + // Allow all core roles + let options = roles.map(role => ({ + name: enrichLabel(role.name), + _id: role._id, + })) + + // Add creator if required + if (allowCreator) { + options.unshift({ + _id: Constants.Roles.CREATOR, + name: "Can edit", + tag: + !$licensing.perAppBuildersEnabled && + capitalise(Constants.PlanType.BUSINESS), + }) + } + + // Add remove option if required if (allowRemove) { - newRoles = [ - ...newRoles, - { - _id: RemoveID, - name: "Remove", - }, - ] + options.push({ + _id: RemoveID, + name: "Remove", + }) } - if (allowPublic) { - return newRoles + + // Remove public if not allowed + if (!allowPublic) { + options = options.filter(role => role._id !== Constants.Roles.PUBLIC) } - return newRoles.filter(role => role._id !== Constants.Roles.PUBLIC) + + return options } const getColor = role => { - if (allowRemove && role._id === RemoveID) { + // Creator and remove options have no colors + if (role._id === Constants.Roles.CREATOR || role._id === RemoveID) { return null } return RoleUtils.getRoleColour(role._id) } const getIcon = role => { - if (allowRemove && role._id === RemoveID) { + // Only remove option has an icon + if (role._id === RemoveID) { return "Close" } return null diff --git a/packages/builder/src/components/common/bindings/BindingPanel.svelte b/packages/builder/src/components/common/bindings/BindingPanel.svelte index cdc96e87c5..4df26c5d03 100644 --- a/packages/builder/src/components/common/bindings/BindingPanel.svelte +++ b/packages/builder/src/components/common/bindings/BindingPanel.svelte @@ -9,18 +9,18 @@ Heading, Icon, } from "@budibase/bbui" - import { createEventDispatcher, onMount } from "svelte" + import { createEventDispatcher, onMount, getContext } from "svelte" import { isValid, decodeJSBinding, encodeJSBinding, + convertToJS, } from "@budibase/string-templates" import { readableToRuntimeBinding, runtimeToReadableBinding, } from "builderStore/dataBinding" - import { convertToJS } from "@budibase/string-templates" import { admin } from "stores/portal" import CodeEditor from "../CodeEditor/CodeEditor.svelte" import { @@ -32,7 +32,6 @@ hbInsert, jsInsert, } from "../CodeEditor" - import { getContext } from "svelte" import BindingPicker from "./BindingPicker.svelte" const dispatch = createEventDispatcher() diff --git a/packages/builder/src/components/common/renderers/CapitaliseRenderer.svelte b/packages/builder/src/components/common/renderers/CapitaliseRenderer.svelte index ea065d217c..b5155fc044 100644 --- a/packages/builder/src/components/common/renderers/CapitaliseRenderer.svelte +++ b/packages/builder/src/components/common/renderers/CapitaliseRenderer.svelte @@ -1,5 +1,6 @@ diff --git a/packages/builder/src/components/common/renderers/DateTimeRenderer.svelte b/packages/builder/src/components/common/renderers/DateTimeRenderer.svelte index 8bf9499b98..c0fb251dc0 100644 --- a/packages/builder/src/components/common/renderers/DateTimeRenderer.svelte +++ b/packages/builder/src/components/common/renderers/DateTimeRenderer.svelte @@ -1,5 +1,6 @@ diff --git a/packages/builder/src/components/deploy/AppActions.svelte b/packages/builder/src/components/deploy/AppActions.svelte index 7259e7e402..7d14fd0e87 100644 --- a/packages/builder/src/components/deploy/AppActions.svelte +++ b/packages/builder/src/components/deploy/AppActions.svelte @@ -20,7 +20,12 @@ import analytics, { Events, EventSource } from "analytics" import { API } from "api" import { apps } from "stores/portal" - import { deploymentStore, store, isOnlyUser } from "builderStore" + import { + deploymentStore, + store, + isOnlyUser, + sortedScreens, + } from "builderStore" import TourWrap from "components/portal/onboarding/TourWrap.svelte" import { TOUR_STEP_KEYS } from "components/portal/onboarding/tours.js" import { goto } from "@roxi/routify" @@ -48,7 +53,7 @@ $store.upgradableVersion && $store.version && $store.upgradableVersion !== $store.version - $: canPublish = !publishing && loaded + $: canPublish = !publishing && loaded && $sortedScreens.length > 0 $: lastDeployed = getLastDeployedString($deploymentStore) const initialiseApp = async () => { @@ -175,7 +180,12 @@
- + Preview
diff --git a/packages/builder/src/components/design/Panel.svelte b/packages/builder/src/components/design/Panel.svelte index 07ec354ae6..ad28018044 100644 --- a/packages/builder/src/components/design/Panel.svelte +++ b/packages/builder/src/components/design/Panel.svelte @@ -11,6 +11,7 @@ export let onClickCloseButton export let borderLeft = false export let borderRight = false + export let borderBottomHeader = true export let wide = false export let extraWide = false export let closeButtonIcon = "Close" @@ -27,7 +28,12 @@ class:borderLeft class:borderRight > -
+
{#if showBackButton} {/if} @@ -95,13 +101,15 @@ justify-content: space-between; align-items: center; padding: 0 var(--spacing-l); - border-bottom: var(--border-light); gap: var(--spacing-m); } .noHeaderBorder { border-bottom: none !important; } + .header.borderBottom { + border-bottom: var(--border-light); + } .title { flex: 1 1 auto; width: 0; diff --git a/packages/builder/src/components/design/settings/componentSettings.js b/packages/builder/src/components/design/settings/componentSettings.js index c2bd08760a..b740247294 100644 --- a/packages/builder/src/components/design/settings/componentSettings.js +++ b/packages/builder/src/components/design/settings/componentSettings.js @@ -20,7 +20,7 @@ import ValidationEditor from "./controls/ValidationEditor/ValidationEditor.svelt import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" import ColumnEditor from "./controls/ColumnEditor/ColumnEditor.svelte" import BasicColumnEditor from "./controls/ColumnEditor/BasicColumnEditor.svelte" -import GridColumnEditor from "./controls/ColumnEditor/GridColumnEditor.svelte" +import GridColumnEditor from "./controls/GridColumnConfiguration/GridColumnConfiguration.svelte" import BarButtonList from "./controls/BarButtonList.svelte" import FieldConfiguration from "./controls/FieldConfiguration/FieldConfiguration.svelte" import ButtonConfiguration from "./controls/ButtonConfiguration/ButtonConfiguration.svelte" @@ -28,6 +28,7 @@ import RelationshipFilterEditor from "./controls/RelationshipFilterEditor.svelte const componentMap = { text: DrawerBindableInput, + plainText: Input, select: Select, radio: RadioGroup, dataSource: DataSourceSelect, diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionEditor.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionEditor.svelte index 6d9e96a564..d445c98a1a 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionEditor.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionEditor.svelte @@ -1,7 +1,6 @@ @@ -17,8 +19,8 @@ x._instanceName} - getOptionValue={x => x._id} + getOptionLabel={x => x.readableBinding} + getOptionValue={x => x.runtimeBinding} />
diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte index 27b6463ffa..9f70272d78 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte @@ -2,29 +2,19 @@ import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui" import { store, currentAsset } from "builderStore" import { tables, viewsV2 } from "stores/backend" - import { - getContextProviderComponents, - getSchemaForDatasourcePlus, - } from "builderStore/dataBinding" + import { getSchemaForDatasourcePlus } from "builderStore/dataBinding" import SaveFields from "./SaveFields.svelte" + import { getDatasourceLikeProviders } from "components/design/settings/controls/ButtonActionEditor/actions/utils" export let parameters export let bindings = [] export let nested - $: formComponents = getContextProviderComponents( - $currentAsset, - $store.selectedComponentId, - "form", - { includeSelf: nested } - ) - $: schemaComponents = getContextProviderComponents( - $currentAsset, - $store.selectedComponentId, - "schema", - { includeSelf: nested } - ) - $: providerOptions = getProviderOptions(formComponents, schemaComponents) + $: providerOptions = getDatasourceLikeProviders({ + asset: $currentAsset, + componentId: $store.selectedComponentId, + nested, + }) $: schemaFields = getSchemaFields(parameters?.tableId) $: tableOptions = $tables.list.map(table => ({ label: table.name, @@ -36,40 +26,6 @@ })) $: options = [...(tableOptions || []), ...(viewOptions || [])] - // Gets a context definition of a certain type from a component definition - const extractComponentContext = (component, contextType) => { - const def = store.actions.components.getDefinition(component?._component) - if (!def) { - return null - } - const contexts = Array.isArray(def.context) ? def.context : [def.context] - return contexts.find(context => context?.type === contextType) - } - - // Gets options for valid context keys which provide valid data to submit - const getProviderOptions = (formComponents, schemaComponents) => { - const formContexts = formComponents.map(component => ({ - component, - context: extractComponentContext(component, "form"), - })) - const schemaContexts = schemaComponents.map(component => ({ - component, - context: extractComponentContext(component, "schema"), - })) - const allContexts = formContexts.concat(schemaContexts) - - return allContexts.map(({ component, context }) => { - let runtimeBinding = component._id - if (context.suffix) { - runtimeBinding += `-${context.suffix}` - } - return { - label: component._instanceName, - value: runtimeBinding, - } - }) - } - const getSchemaFields = resourceId => { const { schema } = getSchemaForDatasourcePlus(resourceId) return Object.values(schema || {}) diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ScrollTo.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ScrollTo.svelte index 49a93d71dd..e73884495d 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ScrollTo.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ScrollTo.svelte @@ -1,22 +1,36 @@
@@ -24,8 +38,8 @@ x._instanceName} - getOptionValue={x => x._id} + getOptionLabel={x => x.readableBinding} + getOptionValue={x => x.runtimeBinding} /> x._instanceName} - getOptionValue={x => x._id} + getOptionLabel={x => x.readableBinding} + getOptionValue={x => x.runtimeBinding} />
diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/utils.js b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/utils.js new file mode 100644 index 0000000000..aa076fdd3e --- /dev/null +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/utils.js @@ -0,0 +1,82 @@ +import { getContextProviderComponents } from "builderStore/dataBinding" +import { store } from "builderStore" +import { capitalise } from "helpers" + +// Generates bindings for all components that provider "datasource like" +// contexts. This includes "form" contexts and "schema" contexts. This is used +// by various button actions as candidates for whole "row" objects. +// Some examples are saving rows or duplicating rows. +export const getDatasourceLikeProviders = ({ asset, componentId, nested }) => { + // Get all form context providers + const formComponents = getContextProviderComponents( + asset, + componentId, + "form", + { includeSelf: nested } + ) + + // Get all schema context providers + const schemaComponents = getContextProviderComponents( + asset, + componentId, + "schema", + { includeSelf: nested } + ) + + // Generate contexts for all form providers + const formContexts = formComponents.map(component => ({ + component, + context: extractComponentContext(component, "form"), + })) + + // Generate contexts for all schema providers + const schemaContexts = schemaComponents.map(component => ({ + component, + context: extractComponentContext(component, "schema"), + })) + + // Check for duplicate contexts by the same component. In this case, attempt + // to label contexts with their suffixes + schemaContexts.forEach(schemaContext => { + // Check if we have a form context for this component + const id = schemaContext.component._id + const existing = formContexts.find(x => x.component._id === id) + if (existing) { + if (existing.context.suffix) { + const suffix = capitalise(existing.context.suffix) + existing.readableSuffix = ` - ${suffix}` + } + if (schemaContext.context.suffix) { + const suffix = capitalise(schemaContext.context.suffix) + schemaContext.readableSuffix = ` - ${suffix}` + } + } + }) + + // Generate bindings for all contexts + const allContexts = formContexts.concat(schemaContexts) + return allContexts.map(({ component, context, readableSuffix }) => { + let readableBinding = component._instanceName + let runtimeBinding = component._id + if (context.suffix) { + runtimeBinding += `-${context.suffix}` + } + if (readableSuffix) { + readableBinding += readableSuffix + } + return { + label: readableBinding, + value: runtimeBinding, + } + }) +} + +// Gets a context definition of a certain type from a component definition +const extractComponentContext = (component, contextType) => { + const def = store.actions.components.getDefinition(component?._component) + if (!def) { + return null + } + const contexts = Array.isArray(def.context) ? def.context : [def.context] + return contexts.find(context => context?.type === contextType) +} diff --git a/packages/builder/src/components/design/settings/controls/ColumnEditor/GridColumnEditor.svelte b/packages/builder/src/components/design/settings/controls/ColumnEditor/GridColumnEditor.svelte deleted file mode 100644 index 291a1b61a8..0000000000 --- a/packages/builder/src/components/design/settings/controls/ColumnEditor/GridColumnEditor.svelte +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/packages/builder/src/components/design/settings/controls/DataSourceSelect/DataSourceSelect.svelte b/packages/builder/src/components/design/settings/controls/DataSourceSelect/DataSourceSelect.svelte index 5cda0ebcca..25aa37365c 100644 --- a/packages/builder/src/components/design/settings/controls/DataSourceSelect/DataSourceSelect.svelte +++ b/packages/builder/src/components/design/settings/controls/DataSourceSelect/DataSourceSelect.svelte @@ -24,8 +24,9 @@ queries as queriesStore, viewsV2 as viewsV2Store, views as viewsStore, + datasources, + integrations, } from "stores/backend" - import { datasources, integrations } from "stores/backend" import BindingBuilder from "components/integration/QueryBindingBuilder.svelte" import IntegrationQueryEditor from "components/integration/index.svelte" import { makePropSafe as safe } from "@budibase/string-templates" diff --git a/packages/builder/src/components/design/settings/controls/DraggableList/DraggableList.svelte b/packages/builder/src/components/design/settings/controls/DraggableList/DraggableList.svelte index 1992299e90..69d3c382cd 100644 --- a/packages/builder/src/components/design/settings/controls/DraggableList/DraggableList.svelte +++ b/packages/builder/src/components/design/settings/controls/DraggableList/DraggableList.svelte @@ -1,8 +1,7 @@ + +
+
+ +
+ + {item.field} +
+
+
{item.label || item.field}
+
+
+ +
+
+ + diff --git a/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/GridColumnConfiguration.svelte b/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/GridColumnConfiguration.svelte new file mode 100644 index 0000000000..4286328367 --- /dev/null +++ b/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/GridColumnConfiguration.svelte @@ -0,0 +1,107 @@ + + +{#if columns.primary} +
+
+
+ columns.update(e.detail)} + /> +
+
+
+{/if} + columns.updateSortable(e.detail)} + on:itemChange={e => columns.update(e.detail)} + items={columns.sortable} + listItemKey={"_id"} + listType={FieldSetting} +/> + + diff --git a/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/PrimaryColumnFieldSetting.svelte b/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/PrimaryColumnFieldSetting.svelte new file mode 100644 index 0000000000..1cb29ac6e7 --- /dev/null +++ b/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/PrimaryColumnFieldSetting.svelte @@ -0,0 +1,100 @@ + + +
+
+ +
+ + {item.field} +
+
+
{item.label || item.field}
+
+
+ +
+
+ + diff --git a/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/getColumns.js b/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/getColumns.js new file mode 100644 index 0000000000..72fdbe4108 --- /dev/null +++ b/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/getColumns.js @@ -0,0 +1,129 @@ +const modernize = columns => { + if (!columns) { + return [] + } + // If the first element has no active key then it's safe to assume all elements are in the old format + if (columns?.[0] && columns[0].active === undefined) { + return columns.map(column => ({ + label: column.displayName, + field: column.name, + active: true, + })) + } + + return columns +} + +const removeInvalidAddMissing = ( + columns = [], + defaultColumns, + primaryDisplayColumnName +) => { + const defaultColumnNames = defaultColumns.map(column => column.field) + const columnNames = columns.map(column => column.field) + + const validColumns = columns.filter(column => + defaultColumnNames.includes(column.field) + ) + let missingColumns = defaultColumns.filter( + defaultColumn => !columnNames.includes(defaultColumn.field) + ) + + // If the user already has fields selected, any appended missing fields should be disabled by default + if (validColumns.length) { + missingColumns = missingColumns.map(field => ({ ...field, active: false })) + } + + const combinedColumns = [...validColumns, ...missingColumns] + + // Ensure the primary display column is always visible + const primaryDisplayIndex = combinedColumns.findIndex( + column => column.field === primaryDisplayColumnName + ) + if (primaryDisplayIndex > -1) { + combinedColumns[primaryDisplayIndex].active = true + } + + return combinedColumns +} + +const getDefault = (schema = {}) => { + const defaultValues = Object.values(schema) + .filter(column => !column.nestedJSON) + .map(column => ({ + label: column.name, + field: column.name, + active: column.visible ?? true, + order: column.visible ? column.order ?? -1 : Number.MAX_SAFE_INTEGER, + })) + + defaultValues.sort((a, b) => a.order - b.order) + + return defaultValues +} + +const toGridFormat = draggableListColumns => { + return draggableListColumns.map(entry => ({ + label: entry.label, + field: entry.field, + active: entry.active, + })) +} + +const toDraggableListFormat = (gridFormatColumns, createComponent, schema) => { + return gridFormatColumns.map(column => { + return createComponent( + "@budibase/standard-components/labelfield", + { + _instanceName: column.field, + active: column.active, + field: column.field, + label: column.label, + columnType: schema[column.field].type, + }, + {} + ) + }) +} + +const getColumns = ({ + columns, + schema, + primaryDisplayColumnName, + onChange, + createComponent, +}) => { + const validatedColumns = removeInvalidAddMissing( + modernize(columns), + getDefault(schema), + primaryDisplayColumnName + ) + const draggableList = toDraggableListFormat( + validatedColumns, + createComponent, + schema + ) + const primary = draggableList.find( + entry => entry.field === primaryDisplayColumnName + ) + const sortable = draggableList.filter( + entry => entry.field !== primaryDisplayColumnName + ) + + return { + primary, + sortable, + updateSortable: newDraggableList => { + onChange(toGridFormat(newDraggableList.concat(primary))) + }, + update: newEntry => { + const newDraggableList = draggableList.map(entry => { + return newEntry.field === entry.field ? newEntry : entry + }) + + onChange(toGridFormat(newDraggableList)) + }, + } +} + +export default getColumns diff --git a/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/getColumns.test.js b/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/getColumns.test.js new file mode 100644 index 0000000000..d7092a2c52 --- /dev/null +++ b/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/getColumns.test.js @@ -0,0 +1,374 @@ +import { it, expect, describe, beforeEach, vi } from "vitest" +import getColumns from "./getColumns" + +describe("getColumns", () => { + beforeEach(ctx => { + ctx.schema = { + one: { name: "one", visible: false, order: 0, type: "foo" }, + two: { name: "two", visible: true, order: 1, type: "foo" }, + three: { name: "three", visible: true, order: 2, type: "foo" }, + four: { name: "four", visible: false, order: 3, type: "foo" }, + five: { + name: "excluded", + visible: true, + order: 4, + type: "foo", + nestedJSON: true, + }, + } + + ctx.primaryDisplayColumnName = "four" + ctx.onChange = vi.fn() + ctx.createComponent = (componentName, props) => { + return { componentName, ...props } + } + }) + + describe("nested json fields", () => { + beforeEach(ctx => { + ctx.columns = getColumns({ + columns: null, + schema: ctx.schema, + primaryDisplayColumnName: ctx.primaryDisplayColumnName, + onChange: ctx.onChange, + createComponent: ctx.createComponent, + }) + }) + + it("does not return nested json fields, as the grid cannot display them", ctx => { + expect(ctx.columns.sortable).not.toContainEqual({ + name: "excluded", + visible: true, + order: 4, + type: "foo", + nestedJSON: true, + }) + }) + }) + + describe("using the old grid column format", () => { + beforeEach(ctx => { + const oldGridFormatColumns = [ + { displayName: "three label", name: "three" }, + { displayName: "two label", name: "two" }, + ] + + ctx.columns = getColumns({ + columns: oldGridFormatColumns, + schema: ctx.schema, + primaryDisplayColumnName: ctx.primaryDisplayColumnName, + onChange: ctx.onChange, + createComponent: ctx.createComponent, + }) + }) + + it("returns the selected and unselected fields in the modern format, respecting the original order", ctx => { + expect(ctx.columns.sortable).toEqual([ + { + _instanceName: "three", + active: true, + columnType: "foo", + componentName: "@budibase/standard-components/labelfield", + field: "three", + label: "three label", + }, + { + _instanceName: "two", + active: true, + columnType: "foo", + componentName: "@budibase/standard-components/labelfield", + field: "two", + label: "two label", + }, + { + _instanceName: "one", + active: false, + columnType: "foo", + componentName: "@budibase/standard-components/labelfield", + field: "one", + label: "one", + }, + ]) + + expect(ctx.columns.primary).toEqual({ + _instanceName: "four", + active: true, + columnType: "foo", + componentName: "@budibase/standard-components/labelfield", + field: "four", + label: "four", + }) + }) + }) + + describe("default columns", () => { + beforeEach(ctx => { + ctx.columns = getColumns({ + columns: undefined, + schema: ctx.schema, + primaryDisplayColumnName: ctx.primaryDisplayColumnName, + onChange: ctx.onChange, + createComponent: ctx.createComponent, + }) + }) + + it("returns all columns, with non-hidden columns automatically selected", ctx => { + expect(ctx.columns.sortable).toEqual([ + { + _instanceName: "two", + active: true, + columnType: "foo", + componentName: "@budibase/standard-components/labelfield", + field: "two", + label: "two", + }, + { + _instanceName: "three", + active: true, + columnType: "foo", + componentName: "@budibase/standard-components/labelfield", + field: "three", + label: "three", + }, + { + _instanceName: "one", + active: false, + columnType: "foo", + componentName: "@budibase/standard-components/labelfield", + field: "one", + label: "one", + }, + ]) + + expect(ctx.columns.primary).toEqual({ + _instanceName: "four", + active: true, + columnType: "foo", + componentName: "@budibase/standard-components/labelfield", + field: "four", + label: "four", + }) + }) + + it("Unselected columns should be placed at the end", ctx => { + expect(ctx.columns.sortable[2].field).toEqual("one") + }) + }) + + describe("missing columns", () => { + beforeEach(ctx => { + const gridFormatColumns = [ + { label: "three label", field: "three", active: true }, + ] + + ctx.columns = getColumns({ + columns: gridFormatColumns, + schema: ctx.schema, + primaryDisplayColumnName: ctx.primaryDisplayColumnName, + onChange: ctx.onChange, + createComponent: ctx.createComponent, + }) + }) + + it("returns all columns, including those missing from the initial data", ctx => { + expect(ctx.columns.sortable).toEqual([ + { + _instanceName: "three", + active: true, + columnType: "foo", + componentName: "@budibase/standard-components/labelfield", + field: "three", + label: "three label", + }, + { + _instanceName: "two", + active: false, + columnType: "foo", + componentName: "@budibase/standard-components/labelfield", + field: "two", + label: "two", + }, + { + _instanceName: "one", + active: false, + columnType: "foo", + componentName: "@budibase/standard-components/labelfield", + field: "one", + label: "one", + }, + ]) + + expect(ctx.columns.primary).toEqual({ + _instanceName: "four", + active: true, + columnType: "foo", + componentName: "@budibase/standard-components/labelfield", + field: "four", + label: "four", + }) + }) + }) + + describe("invalid columns", () => { + beforeEach(ctx => { + const gridFormatColumns = [ + { label: "three label", field: "three", active: true }, + { label: "some nonsense", field: "some nonsense", active: true }, + ] + + ctx.columns = getColumns({ + columns: gridFormatColumns, + schema: ctx.schema, + primaryDisplayColumnName: ctx.primaryDisplayColumnName, + onChange: ctx.onChange, + createComponent: ctx.createComponent, + }) + }) + + it("returns all valid columns, excluding those that aren't valid for the schema", ctx => { + expect(ctx.columns.sortable).toEqual([ + { + _instanceName: "three", + active: true, + columnType: "foo", + componentName: "@budibase/standard-components/labelfield", + field: "three", + label: "three label", + }, + { + _instanceName: "two", + active: false, + columnType: "foo", + componentName: "@budibase/standard-components/labelfield", + field: "two", + label: "two", + }, + { + _instanceName: "one", + active: false, + columnType: "foo", + componentName: "@budibase/standard-components/labelfield", + field: "one", + label: "one", + }, + ]) + + expect(ctx.columns.primary).toEqual({ + _instanceName: "four", + active: true, + columnType: "foo", + componentName: "@budibase/standard-components/labelfield", + field: "four", + label: "four", + }) + }) + }) + + describe("methods", () => { + beforeEach(ctx => { + const { update, updateSortable } = getColumns({ + columns: [], + schema: ctx.schema, + primaryDisplayColumnName: ctx.primaryDisplayColumnName, + onChange: ctx.onChange, + createComponent: ctx.createComponent, + }) + + ctx.update = update + ctx.updateSortable = updateSortable + }) + + describe("update", () => { + beforeEach(ctx => { + ctx.update({ + field: "one", + label: "a new label", + active: true, + }) + }) + + it("calls the callback with the updated columns", ctx => { + expect(ctx.onChange).toHaveBeenCalledTimes(1) + expect(ctx.onChange).toHaveBeenCalledWith([ + { + field: "two", + label: "two", + active: true, + }, + { + field: "three", + label: "three", + active: true, + }, + { + field: "one", + label: "a new label", + active: true, + }, + { + field: "four", + label: "four", + active: true, + }, + ]) + }) + }) + + describe("updateSortable", () => { + beforeEach(ctx => { + ctx.updateSortable([ + { + _instanceName: "three", + active: true, + columnType: "foo", + componentName: "@budibase/standard-components/labelfield", + field: "three", + label: "three", + }, + { + _instanceName: "one", + active: true, + columnType: "foo", + componentName: "@budibase/standard-components/labelfield", + field: "one", + label: "one", + }, + { + _instanceName: "two", + active: false, + columnType: "foo", + componentName: "@budibase/standard-components/labelfield", + field: "two", + label: "two", + }, + ]) + }) + + it("calls the callback with the updated columns", ctx => { + expect(ctx.onChange).toHaveBeenCalledTimes(1) + expect(ctx.onChange).toHaveBeenCalledWith([ + { + field: "three", + label: "three", + active: true, + }, + { + field: "one", + label: "one", + active: true, + }, + { + field: "two", + label: "two", + active: false, + }, + { + field: "four", + label: "four", + active: true, + }, + ]) + }) + }) + }) +}) diff --git a/packages/builder/src/components/integration/QueryViewerBindingBuilder.svelte b/packages/builder/src/components/integration/QueryViewerBindingBuilder.svelte index 3d74e3f6b6..f8a14a6dd1 100644 --- a/packages/builder/src/components/integration/QueryViewerBindingBuilder.svelte +++ b/packages/builder/src/components/integration/QueryViewerBindingBuilder.svelte @@ -1,6 +1,7 @@ diff --git a/packages/builder/src/components/usage/Usage.svelte b/packages/builder/src/components/usage/Usage.svelte index 23d8ddc2f3..897a7da3dd 100644 --- a/packages/builder/src/components/usage/Usage.svelte +++ b/packages/builder/src/components/usage/Usage.svelte @@ -2,6 +2,7 @@ import { Body, ProgressBar, Heading, Icon, Link } from "@budibase/bbui" import { admin, auth } from "../../stores/portal" import { onMount } from "svelte" + export let usage export let warnWhenFull = false diff --git a/packages/builder/src/helpers/keyUtils.js b/packages/builder/src/helpers/keyUtils.js new file mode 100644 index 0000000000..8d6dfb06dc --- /dev/null +++ b/packages/builder/src/helpers/keyUtils.js @@ -0,0 +1,7 @@ +function handleEnter(fnc) { + return e => e.key === "Enter" && fnc() +} + +export const keyUtils = { + handleEnter, +} diff --git a/packages/builder/src/pages/builder/admin/index.svelte b/packages/builder/src/pages/builder/admin/index.svelte index ede9d85808..9723c6b621 100644 --- a/packages/builder/src/pages/builder/admin/index.svelte +++ b/packages/builder/src/pages/builder/admin/index.svelte @@ -1,10 +1,17 @@ @@ -650,8 +659,9 @@ autoWidth align="right" allowedRoles={user.isAdminOrGlobalBuilder - ? [Constants.Roles.ADMIN] + ? [Constants.Roles.CREATOR] : null} + labelPrefix="Can use as" />
@@ -695,19 +705,16 @@ allowRemove={group.role} allowPublic={false} quiet={true} - allowCreator={true} + allowCreator={group.role === Constants.Roles.CREATOR} on:change={e => { - if (e.detail === Constants.Roles.CREATOR) { - addGroupAppBuilder(group._id) - } else { - onUpdateGroup(group, e.detail) - } + onUpdateGroup(group, e.detail) }} on:remove={() => { onUpdateGroup(group) }} autoWidth align="right" + labelPrefix="Can use as" />
@@ -753,6 +760,7 @@ allowedRoles={user.isAdminOrGlobalBuilder ? [Constants.Roles.CREATOR] : null} + labelPrefix="Can use as" /> @@ -781,7 +789,7 @@ {/if} {:else} - +
@@ -804,31 +812,34 @@ option.value !== Constants.BudibaseRoles.Admin )} label="Role" + on:change={checkAppAccess} /> - {#if creationRoleType !== Constants.BudibaseRoles.Admin} + - {/if} + - {#if creationRoleType === Constants.BudibaseRoles.Admin} -
- - Admins will get full access to all apps and settings -
- {/if}