1
0
Fork 0
mirror of synced 2024-08-14 17:42:01 +12:00

Merge remote-tracking branch 'origin/master' into fix/pc-bug-fixes

This commit is contained in:
Peter Clement 2023-11-27 10:16:10 +00:00
commit 4baab262d5
302 changed files with 3247 additions and 1452 deletions

View file

@ -57,7 +57,10 @@
"destructuredArrayIgnorePattern": "^_" "destructuredArrayIgnorePattern": "^_"
} }
], ],
"import/no-relative-packages": "error" "import/no-relative-packages": "error",
"import/export": "error",
"import/no-duplicates": "error",
"import/newline-after-import": "error"
}, },
"globals": { "globals": {
"GeolocationPositionError": true "GeolocationPositionError": true

View file

@ -29,7 +29,6 @@ WORKDIR /opt/couchdb
ADD couch/vm.args couch/local.ini ./etc/ ADD couch/vm.args couch/local.ini ./etc/
WORKDIR / WORKDIR /
ADD build-target-paths.sh .
ADD runner.sh ./bbcouch-runner.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"] CMD ["./bbcouch-runner.sh"]

View file

@ -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

View file

@ -1,14 +1,52 @@
#!/bin/bash #!/bin/bash
DATA_DIR=${DATA_DIR:-/data} DATA_DIR=${DATA_DIR:-/data}
mkdir -p ${DATA_DIR} mkdir -p ${DATA_DIR}
mkdir -p ${DATA_DIR}/couch/{dbs,views} mkdir -p ${DATA_DIR}/couch/{dbs,views}
mkdir -p ${DATA_DIR}/search mkdir -p ${DATA_DIR}/search
chown -R couchdb:couchdb ${DATA_DIR}/couch 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 & /opt/clouseau/bin/clouseau > /dev/stdout 2>&1 &
/docker-entrypoint.sh /opt/couchdb/bin/couchdb & /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/_users
curl -X PUT http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984/_replicator curl -X PUT http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984/_replicator
sleep infinity sleep infinity

View file

@ -6,7 +6,7 @@ services:
app-service: app-service:
build: build:
context: .. context: ..
dockerfile: packages/server/Dockerfile.v2 dockerfile: packages/server/Dockerfile
args: args:
- BUDIBASE_VERSION=0.0.0+dev-docker - BUDIBASE_VERSION=0.0.0+dev-docker
container_name: build-bbapps container_name: build-bbapps
@ -36,7 +36,7 @@ services:
worker-service: worker-service:
build: build:
context: .. context: ..
dockerfile: packages/worker/Dockerfile.v2 dockerfile: packages/worker/Dockerfile
args: args:
- BUDIBASE_VERSION=0.0.0+dev-docker - BUDIBASE_VERSION=0.0.0+dev-docker
container_name: build-bbworker container_name: build-bbworker

View file

@ -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

View file

@ -1,44 +1,59 @@
FROM node:18-slim as build FROM node:18-slim as build
# install node-gyp dependencies # 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 WORKDIR /app
ADD packages/server . COPY package.json .
COPY yarn.lock . COPY yarn.lock .
RUN yarn install --production=true --network-timeout 1000000 COPY lerna.json .
RUN /cleanup.sh COPY .yarnrc .
# build worker COPY packages/server/package.json packages/server/package.json
WORKDIR /worker COPY packages/worker/package.json packages/worker/package.json
ADD packages/worker . # string-templates does not get bundled during the esbuild process, so we want to use the local version
COPY yarn.lock . COPY packages/string-templates/package.json packages/string-templates/package.json
RUN yarn install --production=true --network-timeout 1000000
RUN /cleanup.sh
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 ARG TARGETARCH
ENV TARGETARCH $TARGETARCH ENV TARGETARCH $TARGETARCH
ENV NODE_MAJOR 18
#TARGETBUILD can be set to single (for single docker image) or aas (for azure app service) #TARGETBUILD can be set to single (for single docker image) or aas (for azure app service)
# e.g. docker build --build-arg TARGETBUILD=aas .... # e.g. docker build --build-arg TARGETBUILD=aas ....
ARG TARGETBUILD=single ARG TARGETBUILD=single
ENV TARGETBUILD $TARGETBUILD ENV TARGETBUILD $TARGETBUILD
COPY --from=build /app /app
COPY --from=build /worker /worker
# install base dependencies # install base dependencies
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y --no-install-recommends software-properties-common nginx uuid-runtime redis-server apt-get install -y --no-install-recommends software-properties-common nginx uuid-runtime redis-server libaio1
# Install postgres client for pg_dump utils # 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 \ && 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 \ && 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 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 # install other dependencies, nodejs, oracle requirements, jdk8, redis, nginx
WORKDIR /nodejs WORKDIR /nodejs
RUN curl -sL https://deb.nodesource.com/setup_16.x -o /tmp/nodesource_setup.sh && \ COPY scripts/install-node.sh ./install.sh
bash /tmp/nodesource_setup.sh && \ RUN chmod +x install.sh && ./install.sh
apt-get install -y --no-install-recommends libaio1 nodejs && \
npm install --global yarn pm2
# setup nginx # setup nginx
ADD hosting/single/nginx/nginx.conf /etc/nginx COPY 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-default-site.conf /etc/nginx/sites-enabled/default
RUN mkdir -p /var/log/nginx && \ RUN mkdir -p /var/log/nginx && \
touch /var/log/nginx/error.log && \ touch /var/log/nginx/error.log && \
touch /var/run/nginx.pid && \ touch /var/run/nginx.pid && \
@ -62,29 +75,39 @@ RUN mkdir -p /var/log/nginx && \
WORKDIR / WORKDIR /
RUN mkdir -p scripts/integrations/oracle 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 RUN /bin/bash -e ./scripts/integrations/oracle/instantclient/linux/install.sh
# setup minio # setup minio
WORKDIR /minio WORKDIR /minio
ADD scripts/install-minio.sh ./install.sh COPY scripts/install-minio.sh ./install.sh
RUN chmod +x install.sh && ./install.sh RUN chmod +x install.sh && ./install.sh
# setup runner file # setup runner file
WORKDIR / WORKDIR /
ADD hosting/single/runner.sh . COPY hosting/single/runner.sh .
RUN chmod +x ./runner.sh RUN chmod +x ./runner.sh
ADD hosting/single/healthcheck.sh . COPY hosting/single/healthcheck.sh .
RUN chmod +x ./healthcheck.sh RUN chmod +x ./healthcheck.sh
# Script below sets the path for storing data based on $DATA_DIR # Script below sets the path for storing data based on $DATA_DIR
# For Azure App Service install SSH & point data locations to /home # For Azure App Service install SSH & point data locations to /home
ADD hosting/single/ssh/sshd_config /etc/ COPY hosting/single/ssh/sshd_config /etc/
ADD hosting/single/ssh/ssh_setup.sh /tmp 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
# cleanup cache
RUN yarn cache clean -f
EXPOSE 80 EXPOSE 80
EXPOSE 443 EXPOSE 443
@ -92,20 +115,10 @@ EXPOSE 443
EXPOSE 2222 EXPOSE 2222
VOLUME /data VOLUME /data
# setup letsencrypt certificate ARG BUDIBASE_VERSION
RUN apt-get install -y certbot python3-certbot-nginx # Ensuring the version argument is sent
ADD hosting/letsencrypt /app/letsencrypt RUN test -n "$BUDIBASE_VERSION"
RUN chmod +x /app/letsencrypt/certificate-request.sh /app/letsencrypt/certificate-renew.sh ENV BUDIBASE_VERSION=$BUDIBASE_VERSION
# 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/*
HEALTHCHECK --interval=15s --timeout=15s --start-period=45s CMD "/healthcheck.sh" HEALTHCHECK --interval=15s --timeout=15s --start-period=45s CMD "/healthcheck.sh"

View file

@ -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"]

View file

@ -25,7 +25,7 @@ if [[ $(curl -s -w "%{http_code}\n" http://localhost:4002/health -o /dev/null) -
healthy=false healthy=false
fi 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'; echo 'ERROR: CouchDB is not running';
healthy=false healthy=false
fi fi

View file

@ -22,11 +22,11 @@ declare -a DOCKER_VARS=("APP_PORT" "APPS_URL" "ARCHITECTURE" "BUDIBASE_ENVIRONME
# Azure App Service customisations # Azure App Service customisations
if [[ "${TARGETBUILD}" = "aas" ]]; then if [[ "${TARGETBUILD}" = "aas" ]]; then
DATA_DIR="${DATA_DIR:-/home}" export DATA_DIR="${DATA_DIR:-/home}"
WEBSITES_ENABLE_APP_SERVICE_STORAGE=true WEBSITES_ENABLE_APP_SERVICE_STORAGE=true
/etc/init.d/ssh start /etc/init.d/ssh start
else else
DATA_DIR=${DATA_DIR:-/data} export DATA_DIR=${DATA_DIR:-/data}
fi fi
mkdir -p ${DATA_DIR} mkdir -p ${DATA_DIR}
# Mount NFS or GCP Filestore if env vars exist for it # Mount NFS or GCP Filestore if env vars exist for it

View file

@ -1,5 +1,5 @@
{ {
"version": "2.13.12", "version": "2.13.17",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View file

@ -1,5 +1,6 @@
const _passport = require("koa-passport") const _passport = require("koa-passport")
const LocalStrategy = require("passport-local").Strategy const LocalStrategy = require("passport-local").Strategy
import { getGlobalDB } from "../context" import { getGlobalDB } from "../context"
import { Cookie } from "../constants" import { Cookie } from "../constants"
import { getSessionsForUser, invalidateSessions } from "../security/sessions" import { getSessionsForUser, invalidateSessions } from "../security/sessions"
@ -26,6 +27,7 @@ import { clearCookie, getCookie } from "../utils"
import { ssoSaveUserNoOp } from "../middleware/passport/sso/sso" import { ssoSaveUserNoOp } from "../middleware/passport/sso/sso"
const refresh = require("passport-oauth2-refresh") const refresh = require("passport-oauth2-refresh")
export { export {
auditLog, auditLog,
authError, authError,

View file

@ -17,7 +17,6 @@ import { DocumentType, SEPARATOR } from "../constants"
import { CacheKey, TTL, withCache } from "../cache" import { CacheKey, TTL, withCache } from "../cache"
import * as context from "../context" import * as context from "../context"
import env from "../environment" import env from "../environment"
import environment from "../environment"
// UTILS // UTILS
@ -181,10 +180,10 @@ export async function getGoogleDatasourceConfig(): Promise<
} }
export function getDefaultGoogleConfig(): GoogleInnerConfig | undefined { export function getDefaultGoogleConfig(): GoogleInnerConfig | undefined {
if (environment.GOOGLE_CLIENT_ID && environment.GOOGLE_CLIENT_SECRET) { if (env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) {
return { return {
clientID: environment.GOOGLE_CLIENT_ID!, clientID: env.GOOGLE_CLIENT_ID!,
clientSecret: environment.GOOGLE_CLIENT_SECRET!, clientSecret: env.GOOGLE_CLIENT_SECRET!,
activated: true, activated: true,
} }
} }

View file

@ -1,4 +1,5 @@
import { prefixed, DocumentType } from "@budibase/types" import { prefixed, DocumentType } from "@budibase/types"
export { export {
SEPARATOR, SEPARATOR,
UNICODE_MAX, UNICODE_MAX,

View file

@ -6,6 +6,7 @@ import { AppState, DeletedApp, getAppMetadata } from "../cache/appMetadata"
import { isDevApp, isDevAppID, getProdAppID } from "../docIds/conversions" import { isDevApp, isDevAppID, getProdAppID } from "../docIds/conversions"
import { App, Database } from "@budibase/types" import { App, Database } from "@budibase/types"
import { getStartEndKeyURL } from "../docIds" import { getStartEndKeyURL } from "../docIds"
export * from "../docIds" export * from "../docIds"
/** /**

View file

@ -1,5 +1,6 @@
import { APP_DEV_PREFIX, APP_PREFIX } from "../constants" import { APP_DEV_PREFIX, APP_PREFIX } from "../constants"
import { App } from "@budibase/types" import { App } from "@budibase/types"
const NO_APP_ERROR = "No app provided" const NO_APP_ERROR = "No app provided"
export function isDevAppID(appId?: string) { export function isDevAppID(appId?: string) {

View file

@ -1,2 +1,3 @@
import PosthogProcessor from "./PosthogProcessor" import PosthogProcessor from "./PosthogProcessor"
export default PosthogProcessor export default PosthogProcessor

View file

@ -1,7 +1,9 @@
import { testEnv } from "../../../../../tests/extra" import { testEnv } from "../../../../../tests/extra"
import PosthogProcessor from "../PosthogProcessor" import PosthogProcessor from "../PosthogProcessor"
import { Event, IdentityType, Hosting } from "@budibase/types" import { Event, IdentityType, Hosting } from "@budibase/types"
const tk = require("timekeeper") const tk = require("timekeeper")
import * as cache from "../../../../cache/generic" import * as cache from "../../../../cache/generic"
import { CacheKey } from "../../../../cache/generic" import { CacheKey } from "../../../../cache/generic"
import * as context from "../../../../context" import * as context from "../../../../context"

View file

@ -1,5 +1,6 @@
import env from "../environment" import env from "../environment"
import * as context from "../context" import * as context from "../context"
export * from "./installation" export * from "./installation"
/** /**

View file

@ -38,6 +38,7 @@ export * as docIds from "./docIds"
// circular dependencies // circular dependencies
import * as context from "./context" import * as context from "./context"
import * as _tenancy from "./tenancy" import * as _tenancy from "./tenancy"
export const tenancy = { export const tenancy = {
..._tenancy, ..._tenancy,
...context, ...context,

View file

@ -1,7 +1,6 @@
import { newid } from "./utils" import { newid } from "./utils"
import * as events from "./events" import * as events from "./events"
import { StaticDatabases } from "./db" import { StaticDatabases, doWithDB } from "./db"
import { doWithDB } from "./db"
import { Installation, IdentityType, Database } from "@budibase/types" import { Installation, IdentityType, Database } from "@budibase/types"
import * as context from "./context" import * as context from "./context"
import semver from "semver" import semver from "semver"

View file

@ -1,4 +1,5 @@
import { Header } from "../../constants" import { Header } from "../../constants"
const correlator = require("correlation-id") const correlator = require("correlation-id")
export const setHeader = (headers: any) => { export const setHeader = (headers: any) => {

View file

@ -1,5 +1,6 @@
import { Header } from "../../constants" import { Header } from "../../constants"
import { v4 as uuid } from "uuid" import { v4 as uuid } from "uuid"
const correlator = require("correlation-id") const correlator = require("correlation-id")
const correlation = (ctx: any, next: any) => { const correlation = (ctx: any, next: any) => {

View file

@ -1,9 +1,12 @@
import env from "../../environment" import env from "../../environment"
import { logger } from "./logger" import { logger } from "./logger"
import { IncomingMessage } from "http" import { IncomingMessage } from "http"
const pino = require("koa-pino-logger") const pino = require("koa-pino-logger")
import { Options } from "pino-http" import { Options } from "pino-http"
import { Ctx } from "@budibase/types" import { Ctx } from "@budibase/types"
const correlator = require("correlation-id") const correlator = require("correlation-id")
export function pinoSettings(): Options { export function pinoSettings(): Options {

View file

@ -2,6 +2,7 @@ export * as local from "./passport/local"
export * as google from "./passport/sso/google" export * as google from "./passport/sso/google"
export * as oidc from "./passport/sso/oidc" export * as oidc from "./passport/sso/oidc"
import * as datasourceGoogle from "./passport/datasource/google" import * as datasourceGoogle from "./passport/datasource/google"
export const datasource = { export const datasource = {
google: datasourceGoogle, google: datasourceGoogle,
} }

View file

@ -8,6 +8,7 @@ import {
SaveSSOUserFunction, SaveSSOUserFunction,
GoogleInnerConfig, GoogleInnerConfig,
} from "@budibase/types" } from "@budibase/types"
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
export function buildVerifyFn(saveUserFn: SaveSSOUserFunction) { export function buildVerifyFn(saveUserFn: SaveSSOUserFunction) {

View file

@ -6,6 +6,7 @@ const mockStrategy = require("passport-google-oauth").OAuth2Strategy
jest.mock("../sso") jest.mock("../sso")
import * as _sso from "../sso" import * as _sso from "../sso"
const sso = jest.mocked(_sso) const sso = jest.mocked(_sso)
const mockSaveUserFn = jest.fn() const mockSaveUserFn = jest.fn()

View file

@ -11,6 +11,7 @@ const mockSaveUser = jest.fn()
jest.mock("../../../../users") jest.mock("../../../../users")
import * as _users from "../../../../users" import * as _users from "../../../../users"
const users = jest.mocked(_users) const users = jest.mocked(_users)
const getErrorMessage = () => { const getErrorMessage = () => {

View file

@ -5,6 +5,7 @@ import { structures } from "../../../tests"
import { ContextUser, ServiceType } from "@budibase/types" import { ContextUser, ServiceType } from "@budibase/types"
import { doInAppContext } from "../../context" import { doInAppContext } from "../../context"
import env from "../../environment" import env from "../../environment"
env._set("SERVICE_TYPE", ServiceType.APPS) env._set("SERVICE_TYPE", ServiceType.APPS)
const appId = "app_aaa" const appId = "app_aaa"

View file

@ -1,4 +1,5 @@
const sanitize = require("sanitize-s3-objectkey") const sanitize = require("sanitize-s3-objectkey")
import AWS from "aws-sdk" import AWS from "aws-sdk"
import stream, { Readable } from "stream" import stream, { Readable } from "stream"
import fetch from "node-fetch" import fetch from "node-fetch"

View file

@ -3,6 +3,7 @@ import { getLockClient } from "./init"
import { LockOptions, LockType } from "@budibase/types" import { LockOptions, LockType } from "@budibase/types"
import * as context from "../context" import * as context from "../context"
import env from "../environment" import env from "../environment"
import { logWarn } from "../logging"
async function getClient( async function getClient(
type: LockType, type: LockType,
@ -116,7 +117,7 @@ export async function doWithLock<T>(
const result = await task() const result = await task()
return { executed: true, result } return { executed: true, result }
} catch (e: any) { } catch (e: any) {
console.warn("lock error") logWarn(`lock type: ${opts.type} error`, e)
// lock limit exceeded // lock limit exceeded
if (e.name === "LockError") { if (e.name === "LockError") {
if (opts.type === LockType.TRY_ONCE) { if (opts.type === LockType.TRY_ONCE) {
@ -124,11 +125,9 @@ export async function doWithLock<T>(
// due to retry count (0) exceeded // due to retry count (0) exceeded
return { executed: false } return { executed: false }
} else { } else {
console.error(e)
throw e throw e
} }
} else { } else {
console.error(e)
throw e throw e
} }
} finally { } finally {

View file

@ -160,4 +160,5 @@ export function isPermissionLevelHigherThanRead(level: PermissionLevel) {
// utility as a lot of things need simply the builder permission // utility as a lot of things need simply the builder permission
export const BUILDER = PermissionType.BUILDER export const BUILDER = PermissionType.BUILDER
export const CREATOR = PermissionType.CREATOR
export const GLOBAL_BUILDER = PermissionType.GLOBAL_BUILDER export const GLOBAL_BUILDER = PermissionType.GLOBAL_BUILDER

View file

@ -1,7 +1,12 @@
import { BuiltinPermissionID, PermissionLevel } from "./permissions" 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 { getAppDB } from "../context"
import { doWithDB } from "../db"
import { Screen, Role as RoleDoc } from "@budibase/types" import { Screen, Role as RoleDoc } from "@budibase/types"
import cloneDeep from "lodash/fp/cloneDeep" import cloneDeep from "lodash/fp/cloneDeep"

View file

@ -1,6 +1,7 @@
const redis = require("../redis/init") const redis = require("../redis/init")
const { v4: uuidv4 } = require("uuid") const { v4: uuidv4 } = require("uuid")
const { logWarn } = require("../logging") const { logWarn } = require("../logging")
import env from "../environment" import env from "../environment"
import { import {
Session, Session,

View file

@ -1,9 +1,8 @@
import env from "../environment" import env from "../environment"
import * as eventHelpers from "./events" import * as eventHelpers from "./events"
import * as accounts from "../accounts"
import * as accountSdk from "../accounts" import * as accountSdk from "../accounts"
import * as cache from "../cache" import * as cache from "../cache"
import { getGlobalDB, getIdentity, getTenantId } from "../context" import { doInTenant, getGlobalDB, getIdentity, getTenantId } from "../context"
import * as dbUtils from "../db" import * as dbUtils from "../db"
import { EmailUnavailableError, HTTPError } from "../errors" import { EmailUnavailableError, HTTPError } from "../errors"
import * as platform from "../platform" import * as platform from "../platform"
@ -11,12 +10,10 @@ import * as sessions from "../security/sessions"
import * as usersCore from "./users" import * as usersCore from "./users"
import { import {
Account, Account,
AllDocsResponse,
BulkUserCreated, BulkUserCreated,
BulkUserDeleted, BulkUserDeleted,
isSSOAccount, isSSOAccount,
isSSOUser, isSSOUser,
RowResponse,
SaveUserOpts, SaveUserOpts,
User, User,
UserStatus, UserStatus,
@ -149,12 +146,12 @@ export class UserDB {
static async allUsers() { static async allUsers() {
const db = getGlobalDB() const db = getGlobalDB()
const response = await db.allDocs( const response = await db.allDocs<User>(
dbUtils.getGlobalUserParams(null, { dbUtils.getGlobalUserParams(null, {
include_docs: true, include_docs: true,
}) })
) )
return response.rows.map((row: any) => row.doc) return response.rows.map(row => row.doc!)
} }
static async countUsersByApp(appId: string) { static async countUsersByApp(appId: string) {
@ -212,13 +209,6 @@ export class UserDB {
throw new Error("_id or email is required") 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 let dbUser: User | undefined
if (_id) { if (_id) {
// try to get existing user from db // try to get existing user from db
@ -467,7 +457,7 @@ export class UserDB {
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) { if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
// root account holder can't be deleted from inside budibase // root account holder can't be deleted from inside budibase
const email = dbUser.email const email = dbUser.email
const account = await accounts.getAccount(email) const account = await accountSdk.getAccount(email)
if (account) { if (account) {
if (dbUser.userId === getIdentity()!._id) { if (dbUser.userId === getIdentity()!._id) {
throw new HTTPError('Please visit "Account" to delete this user', 400) throw new HTTPError('Please visit "Account" to delete this user', 400)
@ -488,6 +478,37 @@ export class UserDB {
await sessions.invalidateSessions(userId, { reason: "deletion" }) 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[]) { static async getGroups(groupIds: string[]) {
return await this.groups.getBulk(groupIds) return await this.groups.getBulk(groupIds)
} }

View file

@ -25,6 +25,7 @@ import {
import { getGlobalDB } from "../context" import { getGlobalDB } from "../context"
import * as context from "../context" import * as context from "../context"
import { isCreator } from "./utils" import { isCreator } from "./utils"
import { UserDB } from "./db"
type GetOpts = { cleanup?: boolean } type GetOpts = { cleanup?: boolean }
@ -43,7 +44,7 @@ function removeUserPassword(users: User | User[]) {
return users return users
} }
export const isSupportedUserSearch = (query: SearchQuery) => { export function isSupportedUserSearch(query: SearchQuery) {
const allowed = [ const allowed = [
{ op: SearchQueryOperators.STRING, key: "email" }, { op: SearchQueryOperators.STRING, key: "email" },
{ op: SearchQueryOperators.EQUAL, key: "_id" }, { op: SearchQueryOperators.EQUAL, key: "_id" },
@ -68,10 +69,10 @@ export const isSupportedUserSearch = (query: SearchQuery) => {
return true return true
} }
export const bulkGetGlobalUsersById = async ( export async function bulkGetGlobalUsersById(
userIds: string[], userIds: string[],
opts?: GetOpts opts?: GetOpts
) => { ) {
const db = getGlobalDB() const db = getGlobalDB()
let users = ( let users = (
await db.allDocs({ await db.allDocs({
@ -85,7 +86,7 @@ export const bulkGetGlobalUsersById = async (
return users return users
} }
export const getAllUserIds = async () => { export async function getAllUserIds() {
const db = getGlobalDB() const db = getGlobalDB()
const startKey = `${DocumentType.USER}${SEPARATOR}` const startKey = `${DocumentType.USER}${SEPARATOR}`
const response = await db.allDocs({ const response = await db.allDocs({
@ -95,7 +96,7 @@ export const getAllUserIds = async () => {
return response.rows.map(row => row.id) return response.rows.map(row => row.id)
} }
export const bulkUpdateGlobalUsers = async (users: User[]) => { export async function bulkUpdateGlobalUsers(users: User[]) {
const db = getGlobalDB() const db = getGlobalDB()
return (await db.bulkDocs(users)) as BulkDocsResponse return (await db.bulkDocs(users)) as BulkDocsResponse
} }
@ -113,10 +114,10 @@ export async function getById(id: string, opts?: GetOpts): Promise<User> {
* Given an email address this will use a view to search through * Given an email address this will use a view to search through
* all the users to find one with this email address. * all the users to find one with this email address.
*/ */
export const getGlobalUserByEmail = async ( export async function getGlobalUserByEmail(
email: String, email: String,
opts?: GetOpts opts?: GetOpts
): Promise<User | undefined> => { ): Promise<User | undefined> {
if (email == null) { if (email == null) {
throw "Must supply an email address to view" throw "Must supply an email address to view"
} }
@ -139,11 +140,23 @@ export const getGlobalUserByEmail = async (
return user 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, appId: any,
opts: DatabaseQueryOpts, opts: DatabaseQueryOpts,
getOpts?: GetOpts getOpts?: GetOpts
) => { ) {
if (typeof appId !== "string") { if (typeof appId !== "string") {
throw new Error("Must provide a string based app ID") 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 Return any user who potentially has access to the application
Admins, developers and app users with the explicitly role. Admins, developers and app users with the explicitly role.
*/ */
export const searchGlobalUsersByAppAccess = async ( export async function searchGlobalUsersByAppAccess(
appId: any, appId: any,
opts?: { limit?: number } opts?: { limit?: number }
) => { ) {
const roleSelector = `roles.${appId}` const roleSelector = `roles.${appId}`
let orQuery: any[] = [ let orQuery: any[] = [
@ -205,7 +218,7 @@ export const searchGlobalUsersByAppAccess = async (
return resp.rows return resp.rows
} }
export const getGlobalUserByAppPage = (appId: string, user: User) => { export function getGlobalUserByAppPage(appId: string, user: User) {
if (!user) { if (!user) {
return return
} }
@ -215,11 +228,11 @@ export const getGlobalUserByAppPage = (appId: string, user: User) => {
/** /**
* Performs a starts with search on the global email view. * Performs a starts with search on the global email view.
*/ */
export const searchGlobalUsersByEmail = async ( export async function searchGlobalUsersByEmail(
email: string | unknown, email: string | unknown,
opts: any, opts: any,
getOpts?: GetOpts getOpts?: GetOpts
) => { ) {
if (typeof email !== "string") { if (typeof email !== "string") {
throw new Error("Must provide a string to search by") throw new Error("Must provide a string to search by")
} }
@ -242,12 +255,12 @@ export const searchGlobalUsersByEmail = async (
} }
const PAGE_LIMIT = 8 const PAGE_LIMIT = 8
export const paginatedUsers = async ({ export async function paginatedUsers({
bookmark, bookmark,
query, query,
appId, appId,
limit, limit,
}: SearchUsersRequest = {}) => { }: SearchUsersRequest = {}) {
const db = getGlobalDB() const db = getGlobalDB()
const pageSize = limit ?? PAGE_LIMIT const pageSize = limit ?? PAGE_LIMIT
const pageLimit = pageSize + 1 const pageLimit = pageSize + 1
@ -324,3 +337,20 @@ export function cleanseUserObject(user: User | ContextUser, base?: User) {
} }
return 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 })
}

View file

@ -1,4 +1,5 @@
import env from "../environment" import env from "../environment"
export * from "../docIds/newid" export * from "../docIds/newid"
const bcrypt = env.JS_BCRYPT ? require("bcryptjs") : require("bcrypt") const bcrypt = env.JS_BCRYPT ? require("bcryptjs") : require("bcrypt")

View file

@ -11,6 +11,7 @@ import {
TenantResolutionStrategy, TenantResolutionStrategy,
} from "@budibase/types" } from "@budibase/types"
import type { SetOption } from "cookies" import type { SetOption } from "cookies"
const jwt = require("jsonwebtoken") const jwt = require("jsonwebtoken")
const APP_PREFIX = DocumentType.APP + SEPARATOR const APP_PREFIX = DocumentType.APP + SEPARATOR

View file

@ -1,3 +1,4 @@
jest.mock("../../../../src/logging/alerts") jest.mock("../../../../src/logging/alerts")
import * as _alerts from "../../../../src/logging/alerts" import * as _alerts from "../../../../src/logging/alerts"
export const alerts = jest.mocked(_alerts) export const alerts = jest.mocked(_alerts)

View file

@ -1,5 +1,6 @@
jest.mock("../../../../src/accounts") jest.mock("../../../../src/accounts")
import * as _accounts from "../../../../src/accounts" import * as _accounts from "../../../../src/accounts"
export const accounts = jest.mocked(_accounts) export const accounts = jest.mocked(_accounts)
export * as date from "./date" export * as date from "./date"

View file

@ -1,2 +1,3 @@
import Chance from "./Chance" import Chance from "./Chance"
export const generator = new Chance() export const generator = new Chance()

View file

@ -9,6 +9,7 @@ mocks.fetch.enable()
// mock all dates to 2020-01-01T00:00:00.000Z // mock all dates to 2020-01-01T00:00:00.000Z
// use tk.reset() to use real dates in individual tests // use tk.reset() to use real dates in individual tests
import tk from "timekeeper" import tk from "timekeeper"
tk.freeze(mocks.date.MOCK_DATE) tk.freeze(mocks.date.MOCK_DATE)
if (!process.env.DEBUG) { if (!process.env.DEBUG) {

View file

@ -1,5 +1,6 @@
<script> <script>
import "@spectrum-css/actiongroup/dist/index-vars.css" import "@spectrum-css/actiongroup/dist/index-vars.css"
export let vertical = false export let vertical = false
export let justified = false export let justified = false
export let quiet = false export let quiet = false

View file

@ -1,5 +1,6 @@
<script> <script>
import "@spectrum-css/avatar/dist/index-vars.css" import "@spectrum-css/avatar/dist/index-vars.css"
let sizes = new Map([ let sizes = new Map([
["XXS", "--spectrum-alias-avatar-size-50"], ["XXS", "--spectrum-alias-avatar-size-50"],
["XS", "--spectrum-alias-avatar-size-75"], ["XS", "--spectrum-alias-avatar-size-75"],

View file

@ -1,7 +1,8 @@
<script> <script>
import "@spectrum-css/buttongroup/dist/index-vars.css" import "@spectrum-css/buttongroup/dist/index-vars.css"
export let vertical = false export let vertical = false
export let gap = "" export let gap = "M"
$: gapStyle = $: gapStyle =
gap === "L" gap === "L"

View file

@ -1,5 +1,6 @@
<script> <script>
import "@spectrum-css/divider/dist/index-vars.css" import "@spectrum-css/divider/dist/index-vars.css"
export let size = "M" export let size = "M"
export let vertical = false export let vertical = false

View file

@ -3,8 +3,7 @@
import Button from "../Button/Button.svelte" import Button from "../Button/Button.svelte"
import Body from "../Typography/Body.svelte" import Body from "../Typography/Body.svelte"
import Heading from "../Typography/Heading.svelte" import Heading from "../Typography/Heading.svelte"
import { setContext } from "svelte" import { setContext, createEventDispatcher } from "svelte"
import { createEventDispatcher } from "svelte"
import { generate } from "shortid" import { generate } from "shortid"
export let title export let title

View file

@ -12,11 +12,13 @@
export let error = null export let error = null
export let validate = null export let validate = null
export let options = [] export let options = []
export let footer = null
export let isOptionEnabled = () => true export let isOptionEnabled = () => true
export let getOptionLabel = option => extractProperty(option, "label") export let getOptionLabel = option => extractProperty(option, "label")
export let getOptionValue = option => extractProperty(option, "value") export let getOptionValue = option => extractProperty(option, "value")
export let getOptionSubtitle = option => extractProperty(option, "subtitle") export let getOptionSubtitle = option => extractProperty(option, "subtitle")
export let getOptionColour = () => null export let getOptionColour = () => null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let open = false let open = false
@ -100,6 +102,7 @@
{error} {error}
{disabled} {disabled}
{options} {options}
{footer}
{getOptionLabel} {getOptionLabel}
{getOptionValue} {getOptionValue}
{getOptionSubtitle} {getOptionSubtitle}

View file

@ -17,7 +17,7 @@
export let options = [] export let options = []
export let getOptionLabel = option => extractProperty(option, "label") export let getOptionLabel = option => extractProperty(option, "label")
export let getOptionValue = option => extractProperty(option, "value") export let getOptionValue = option => extractProperty(option, "value")
export let getOptionSubtitle = option => option?.subtitle
export let isOptionSelected = () => false export let isOptionSelected = () => false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -135,7 +135,7 @@
class="spectrum-Textfield-input spectrum-InputGroup-input" class="spectrum-Textfield-input spectrum-InputGroup-input"
/> />
</div> </div>
<div style="width: 30%"> <div style="width: 40%">
<button <button
{id} {id}
class="spectrum-Picker spectrum-Picker--sizeM override-borders" class="spectrum-Picker spectrum-Picker--sizeM override-borders"
@ -157,38 +157,43 @@
<use xlink:href="#spectrum-css-icon-Chevron100" /> <use xlink:href="#spectrum-css-icon-Chevron100" />
</svg> </svg>
</button> </button>
{#if open}
<div
use:clickOutside={handleOutsideClick}
transition:fly|local={{ y: -20, duration: 200 }}
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
>
<ul class="spectrum-Menu" role="listbox">
{#each options as option, idx}
<li
class="spectrum-Menu-item"
class:is-selected={isOptionSelected(getOptionValue(option, idx))}
role="option"
aria-selected="true"
tabindex="0"
on:click={() => onPick(getOptionValue(option, idx))}
>
<span class="spectrum-Menu-itemLabel">
{getOptionLabel(option, idx)}
</span>
<svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-css-icon-Checkmark100" />
</svg>
</li>
{/each}
</ul>
</div>
{/if}
</div> </div>
{#if open}
<div
use:clickOutside={handleOutsideClick}
transition:fly|local={{ y: -20, duration: 200 }}
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
>
<ul class="spectrum-Menu" role="listbox">
{#each options as option, idx}
<li
class="spectrum-Menu-item"
class:is-selected={isOptionSelected(getOptionValue(option, idx))}
role="option"
aria-selected="true"
tabindex="0"
on:click={() => onPick(getOptionValue(option, idx))}
>
<span class="spectrum-Menu-itemLabel">
{getOptionLabel(option, idx)}
{#if getOptionSubtitle(option, idx)}
<span class="subtitle-text">
{getOptionSubtitle(option, idx)}
</span>
{/if}
</span>
<svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-css-icon-Checkmark100" />
</svg>
</li>
{/each}
</ul>
</div>
{/if}
</div> </div>
<style> <style>
@ -196,7 +201,6 @@
min-width: 0; min-width: 0;
width: 100%; width: 100%;
} }
.spectrum-InputGroup-input { .spectrum-InputGroup-input {
border-right-width: 1px; border-right-width: 1px;
} }
@ -206,7 +210,6 @@
.spectrum-Textfield-input { .spectrum-Textfield-input {
width: 0; width: 0;
} }
.override-borders { .override-borders {
border-top-left-radius: 0px; border-top-left-radius: 0px;
border-bottom-left-radius: 0px; border-bottom-left-radius: 0px;
@ -215,5 +218,18 @@
max-height: 240px; max-height: 240px;
z-index: 999; z-index: 999;
top: 100%; top: 100%;
width: 100%;
}
.subtitle-text {
font-size: 12px;
line-height: 15px;
font-weight: 500;
color: var(--spectrum-global-color-gray-600);
display: block;
margin-top: var(--spacing-s);
}
.spectrum-Menu-checkmark {
align-self: center;
margin-top: 0;
} }
</style> </style>

View file

@ -224,13 +224,12 @@
</span> </span>
{/if} {/if}
<span class="spectrum-Menu-itemLabel"> <span class="spectrum-Menu-itemLabel">
{#if getOptionSubtitle(option, idx)}
<span class="subtitle-text"
>{getOptionSubtitle(option, idx)}</span
>
{/if}
{getOptionLabel(option, idx)} {getOptionLabel(option, idx)}
{#if getOptionSubtitle(option, idx)}
<span class="subtitle-text">
{getOptionSubtitle(option, idx)}
</span>
{/if}
</span> </span>
{#if option.tag} {#if option.tag}
<span class="option-tag"> <span class="option-tag">
@ -275,10 +274,9 @@
font-size: 12px; font-size: 12px;
line-height: 15px; line-height: 15px;
font-weight: 500; font-weight: 500;
top: 10px;
color: var(--spectrum-global-color-gray-600); color: var(--spectrum-global-color-gray-600);
display: block; display: block;
margin-bottom: var(--spacing-s); margin-top: var(--spacing-s);
} }
.spectrum-Picker-label.auto-width { .spectrum-Picker-label.auto-width {

View file

@ -10,8 +10,9 @@
export let getOptionLabel = option => option export let getOptionLabel = option => option
export let getOptionValue = option => option export let getOptionValue = option => option
export let getOptionIcon = () => null export let getOptionIcon = () => null
export let useOptionIconImage = false
export let getOptionColour = () => null export let getOptionColour = () => null
export let getOptionSubtitle = () => null
export let useOptionIconImage = false
export let isOptionEnabled export let isOptionEnabled
export let readonly = false export let readonly = false
export let quiet = false export let quiet = false
@ -82,8 +83,9 @@
{getOptionLabel} {getOptionLabel}
{getOptionValue} {getOptionValue}
{getOptionIcon} {getOptionIcon}
{useOptionIconImage}
{getOptionColour} {getOptionColour}
{getOptionSubtitle}
{useOptionIconImage}
{isOptionEnabled} {isOptionEnabled}
{autocomplete} {autocomplete}
{sort} {sort}

View file

@ -43,6 +43,7 @@
{quiet} {quiet}
{autofocus} {autofocus}
{options} {options}
isOptionSelected={option => option === dropdownValue}
on:change={onChange} on:change={onChange}
on:pick={onPick} on:pick={onPick}
on:click on:click

View file

@ -13,9 +13,10 @@
export let options = [] export let options = []
export let getOptionLabel = option => extractProperty(option, "label") export let getOptionLabel = option => extractProperty(option, "label")
export let getOptionValue = option => extractProperty(option, "value") export let getOptionValue = option => extractProperty(option, "value")
export let getOptionSubtitle = option => option?.subtitle
export let getOptionIcon = option => option?.icon export let getOptionIcon = option => option?.icon
export let useOptionIconImage = false
export let getOptionColour = option => option?.colour export let getOptionColour = option => option?.colour
export let useOptionIconImage = false
export let isOptionEnabled export let isOptionEnabled
export let quiet = false export let quiet = false
export let autoWidth = false export let autoWidth = false
@ -58,6 +59,7 @@
{getOptionValue} {getOptionValue}
{getOptionIcon} {getOptionIcon}
{getOptionColour} {getOptionColour}
{getOptionSubtitle}
{useOptionIconImage} {useOptionIconImage}
{isOptionEnabled} {isOptionEnabled}
{autocomplete} {autocomplete}

View file

@ -16,10 +16,9 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onClick = e => { const onClick = () => {
if (!disabled) { if (!disabled) {
dispatch("click") dispatch("click")
e.stopPropagation()
} }
} }
</script> </script>

View file

@ -1,5 +1,6 @@
<script> <script>
import Input from "../Form/Input.svelte" import Input from "../Form/Input.svelte"
let value = "" let value = ""
</script> </script>

View file

@ -4,6 +4,7 @@
import Icon from "../Icon/Icon.svelte" import Icon from "../Icon/Icon.svelte"
import Popover from "../Popover/Popover.svelte" import Popover from "../Popover/Popover.svelte"
import { onMount } from "svelte" import { onMount } from "svelte"
const flipDurationMs = 150 const flipDurationMs = 150
export let constraints export let constraints

View file

@ -1,11 +1,10 @@
<script> <script>
import "@spectrum-css/popover/dist/index-vars.css" import "@spectrum-css/popover/dist/index-vars.css"
import Portal from "svelte-portal" import Portal from "svelte-portal"
import { createEventDispatcher } from "svelte" import { createEventDispatcher, getContext } from "svelte"
import positionDropdown from "../Actions/position_dropdown" import positionDropdown from "../Actions/position_dropdown"
import clickOutside from "../Actions/click_outside" import clickOutside from "../Actions/click_outside"
import { fly } from "svelte/transition" import { fly } from "svelte/transition"
import { getContext } from "svelte"
import Context from "../context" import Context from "../context"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()

View file

@ -1,7 +1,9 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
const multilevel = getContext("sidenav-type") const multilevel = getContext("sidenav-type")
import Badge from "../Badge/Badge.svelte" import Badge from "../Badge/Badge.svelte"
export let href = "" export let href = ""
export let external = false export let external = false
export let heading = "" export let heading = ""

View file

@ -1,6 +1,7 @@
<script> <script>
import { setContext } from "svelte" import { setContext } from "svelte"
import "@spectrum-css/sidenav/dist/index-vars.css" import "@spectrum-css/sidenav/dist/index-vars.css"
export let multilevel = false export let multilevel = false
setContext("sidenav-type", multilevel) setContext("sidenav-type", multilevel)
</script> </script>

View file

@ -1,6 +1,7 @@
<script> <script>
import "@spectrum-css/label/dist/index-vars.css" import "@spectrum-css/label/dist/index-vars.css"
import Badge from "../Badge/Badge.svelte" import Badge from "../Badge/Badge.svelte"
export let value export let value
const displayLimit = 5 const displayLimit = 5

View file

@ -1,6 +1,7 @@
<script> <script>
import { getContext, onMount, createEventDispatcher } from "svelte" import { getContext, onMount, createEventDispatcher } from "svelte"
import Portal from "svelte-portal" import Portal from "svelte-portal"
export let title export let title
export let icon = "" export let icon = ""
export let id export let id

View file

@ -1,4 +1,5 @@
import { helpers } from "@budibase/shared-core" import { helpers } from "@budibase/shared-core"
export const deepGet = helpers.deepGet export const deepGet = helpers.deepGet
/** /**

View file

@ -228,7 +228,12 @@ export const getContextProviderComponents = (
/** /**
* Gets all data provider components above a component. * Gets all data provider components above a component.
*/ */
export const getActionProviderComponents = (asset, componentId, actionType) => { export const getActionProviders = (
asset,
componentId,
actionType,
options = { includeSelf: false }
) => {
if (!asset || !componentId) { if (!asset || !componentId) {
return [] return []
} }
@ -236,13 +241,30 @@ export const getActionProviderComponents = (asset, componentId, actionType) => {
// Get the component tree leading up to this component, ignoring the component // Get the component tree leading up to this component, ignoring the component
// itself // itself
const path = findComponentPath(asset.props, componentId) const path = findComponentPath(asset.props, componentId)
path.pop() if (!options?.includeSelf) {
path.pop()
}
// Filter by only data provider components // Find matching contexts and generate bindings
return path.filter(component => { let providers = []
path.forEach(component => {
const def = store.actions.components.getDefinition(component._component) const def = store.actions.components.getDefinition(component._component)
return def?.actions?.includes(actionType) const actions = (def?.actions || []).map(action => {
return typeof action === "string" ? { type: action } : action
})
const action = actions.find(x => x.type === actionType)
if (action) {
let runtimeBinding = component._id
if (action.suffix) {
runtimeBinding += `-${action.suffix}`
}
providers.push({
readableBinding: component._instanceName,
runtimeBinding,
})
}
}) })
return providers
} }
/** /**
@ -1068,17 +1090,18 @@ export const removeBindings = (obj, replacement = "Invalid binding") => {
* When converting from readable to runtime it can sometimes add too many square brackets, * When converting from readable to runtime it can sometimes add too many square brackets,
* this makes sure that doesn't happen. * this makes sure that doesn't happen.
*/ */
const shouldReplaceBinding = (currentValue, convertFrom, convertTo) => { const shouldReplaceBinding = (currentValue, from, convertTo, binding) => {
if (!currentValue?.includes(convertFrom)) { if (!currentValue?.includes(from)) {
return false return false
} }
if (convertTo === "readableBinding") { if (convertTo === "readableBinding") {
return true // Dont replace if the value already matches the readable binding
return currentValue.indexOf(binding.readableBinding) === -1
} }
// remove all the spaces, if the input is surrounded by spaces e.g. [ Auto ID ] then // remove all the spaces, if the input is surrounded by spaces e.g. [ Auto ID ] then
// this makes sure it is detected // this makes sure it is detected
const noSpaces = currentValue.replace(/\s+/g, "") const noSpaces = currentValue.replace(/\s+/g, "")
const fromNoSpaces = convertFrom.replace(/\s+/g, "") const fromNoSpaces = from.replace(/\s+/g, "")
const invalids = [ const invalids = [
`[${fromNoSpaces}]`, `[${fromNoSpaces}]`,
`"${fromNoSpaces}"`, `"${fromNoSpaces}"`,
@ -1130,8 +1153,11 @@ const bindingReplacement = (
// in the search, working from longest to shortest so always use best match first // in the search, working from longest to shortest so always use best match first
let searchString = newBoundValue let searchString = newBoundValue
for (let from of convertFromProps) { for (let from of convertFromProps) {
if (isJS || shouldReplaceBinding(newBoundValue, from, convertTo)) { const binding = bindableProperties.find(el => el[convertFrom] === from)
const binding = bindableProperties.find(el => el[convertFrom] === from) if (
isJS ||
shouldReplaceBinding(newBoundValue, from, convertTo, binding)
) {
let idx let idx
do { do {
// see if any instances of this binding exist in the search string // see if any instances of this binding exist in the search string

View file

@ -4,11 +4,10 @@ import { getTemporalStore } from "./store/temporal"
import { getThemeStore } from "./store/theme" import { getThemeStore } from "./store/theme"
import { getUserStore } from "./store/users" import { getUserStore } from "./store/users"
import { getDeploymentStore } from "./store/deployments" import { getDeploymentStore } from "./store/deployments"
import { derived, writable } from "svelte/store" import { derived, writable, get } from "svelte/store"
import { findComponent, findComponentPath } from "./componentUtils" import { findComponent, findComponentPath } from "./componentUtils"
import { RoleUtils } from "@budibase/frontend-core" import { RoleUtils } from "@budibase/frontend-core"
import { createHistoryStore } from "builderStore/store/history" import { createHistoryStore } from "builderStore/store/history"
import { get } from "svelte/store"
export const store = getFrontendStore() export const store = getFrontendStore()
export const automationStore = getAutomationStore() export const automationStore = getAutomationStore()

View file

@ -0,0 +1,86 @@
import { expect, describe, it, vi } from "vitest"
import {
runtimeToReadableBinding,
readableToRuntimeBinding,
} from "../dataBinding"
vi.mock("@budibase/frontend-core")
vi.mock("builderStore/componentUtils")
vi.mock("builderStore/store")
vi.mock("builderStore/store/theme")
vi.mock("builderStore/store/temporal")
describe("runtimeToReadableBinding", () => {
const bindableProperties = [
{
category: "Current User",
icon: "User",
providerId: "user",
readableBinding: "Current User.firstName",
runtimeBinding: "[user].[firstName]",
type: "context",
},
{
category: "Bindings",
icon: "Brackets",
readableBinding: "Binding.count",
runtimeBinding: "count",
type: "context",
},
]
it("should convert a runtime binding to a readable one", () => {
const textWithBindings = `Hello {{ [user].[firstName] }}! The count is {{ count }}.`
expect(
runtimeToReadableBinding(
bindableProperties,
textWithBindings,
"readableBinding"
)
).toEqual(
`Hello {{ Current User.firstName }}! The count is {{ Binding.count }}.`
)
})
it("should not convert to readable binding if it is already readable", () => {
const textWithBindings = `Hello {{ [user].[firstName] }}! The count is {{ Binding.count }}.`
expect(
runtimeToReadableBinding(
bindableProperties,
textWithBindings,
"readableBinding"
)
).toEqual(
`Hello {{ Current User.firstName }}! The count is {{ Binding.count }}.`
)
})
})
describe("readableToRuntimeBinding", () => {
const bindableProperties = [
{
category: "Current User",
icon: "User",
providerId: "user",
readableBinding: "Current User.firstName",
runtimeBinding: "[user].[firstName]",
type: "context",
},
{
category: "Bindings",
icon: "Brackets",
readableBinding: "Binding.count",
runtimeBinding: "count",
type: "context",
},
]
it("should convert a readable binding to a runtime one", () => {
const textWithBindings = `Hello {{ Current User.firstName }}! The count is {{ Binding.count }}.`
expect(
readableToRuntimeBinding(
bindableProperties,
textWithBindings,
"runtimeBinding"
)
).toEqual(`Hello {{ [user].[firstName] }}! The count is {{ count }}.`)
})
})

View file

@ -7,11 +7,9 @@ import {
} from "builderStore" } from "builderStore"
import { datasources, tables } from "stores/backend" import { datasources, tables } from "stores/backend"
import { get } from "svelte/store" import { get } from "svelte/store"
import { auth } from "stores/portal" import { auth, apps } from "stores/portal"
import { SocketEvent, BuilderSocketEvent } from "@budibase/shared-core" import { SocketEvent, BuilderSocketEvent, helpers } from "@budibase/shared-core"
import { apps } from "stores/portal"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
import { helpers } from "@budibase/shared-core"
export const createBuilderWebsocket = appId => { export const createBuilderWebsocket = appId => {
const socket = createWebsocket("/socket/builder") const socket = createWebsocket("/socket/builder")

View file

@ -1,5 +1,9 @@
<script> <script>
import { automationStore, selectedAutomation } from "builderStore" import {
automationStore,
selectedAutomation,
automationHistoryStore,
} from "builderStore"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import FlowItem from "./FlowItem.svelte" import FlowItem from "./FlowItem.svelte"
import TestDataModal from "./TestDataModal.svelte" import TestDataModal from "./TestDataModal.svelte"
@ -8,7 +12,6 @@
import { Icon, notifications, Modal } from "@budibase/bbui" import { Icon, notifications, Modal } from "@budibase/bbui"
import { ActionStepID } from "constants/backend/automations" import { ActionStepID } from "constants/backend/automations"
import UndoRedoControl from "components/common/UndoRedoControl.svelte" import UndoRedoControl from "components/common/UndoRedoControl.svelte"
import { automationHistoryStore } from "builderStore"
export let automation export let automation

View file

@ -1,7 +1,7 @@
<script> <script>
import { automationStore } from "builderStore" import { automationStore } from "builderStore"
import { notifications } from "@budibase/bbui"
import { import {
notifications,
Input, Input,
InlineAlert, InlineAlert,
ModalContent, ModalContent,

View file

@ -1,7 +1,12 @@
<script> <script>
import { automationStore } from "builderStore" import { automationStore } from "builderStore"
import { notifications } from "@budibase/bbui" import {
import { Icon, Input, ModalContent, Modal } from "@budibase/bbui" notifications,
Icon,
Input,
ModalContent,
Modal,
} from "@budibase/bbui"
export let automation export let automation
export let onCancel = undefined export let onCancel = undefined

View file

@ -38,12 +38,11 @@
EditorModes, EditorModes,
} from "components/common/CodeEditor" } from "components/common/CodeEditor"
import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte" import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte"
import { LuceneUtils } from "@budibase/frontend-core" import { LuceneUtils, Utils } from "@budibase/frontend-core"
import { import {
getSchemaForDatasourcePlus, getSchemaForDatasourcePlus,
getEnvironmentBindings, getEnvironmentBindings,
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
import { Utils } from "@budibase/frontend-core"
import { TriggerStepID, ActionStepID } from "constants/backend/automations" import { TriggerStepID, ActionStepID } from "constants/backend/automations"
import { onMount } from "svelte" import { onMount } from "svelte"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"

View file

@ -2,6 +2,7 @@
import { Button, Select, Input, Label } from "@budibase/bbui" import { Button, Select, Input, Label } from "@budibase/bbui"
import { onMount, createEventDispatcher } from "svelte" import { onMount, createEventDispatcher } from "svelte"
import { flags } from "stores/backend" import { flags } from "stores/backend"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
export let value export let value

View file

@ -1,8 +1,7 @@
<script> <script>
import { Icon, notifications } from "@budibase/bbui" import { Icon, notifications, ModalContent } from "@budibase/bbui"
import { automationStore, selectedAutomation } from "builderStore" import { automationStore, selectedAutomation } from "builderStore"
import WebhookDisplay from "./WebhookDisplay.svelte" import WebhookDisplay from "./WebhookDisplay.svelte"
import { ModalContent } from "@budibase/bbui"
import { onMount, onDestroy } from "svelte" import { onMount, onDestroy } from "svelte"
const POLL_RATE_MS = 2500 const POLL_RATE_MS = 2500

View file

@ -1,11 +1,15 @@
<script> <script>
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { tables } from "stores/backend" import { tables, roles } from "stores/backend"
import { roles } from "stores/backend" import {
import { notifications } from "@budibase/bbui" notifications,
keepOpen,
ModalContent,
Select,
Link,
} from "@budibase/bbui"
import RowFieldControl from "../RowFieldControl.svelte" import RowFieldControl from "../RowFieldControl.svelte"
import { API } from "api" import { API } from "api"
import { keepOpen, ModalContent, Select, Link } from "@budibase/bbui"
import ErrorsBox from "components/common/ErrorsBox.svelte" import ErrorsBox from "components/common/ErrorsBox.svelte"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"

View file

@ -1,8 +1,14 @@
<script> <script>
import { keepOpen, ModalContent, Select, Input, Button } from "@budibase/bbui" import {
keepOpen,
ModalContent,
Select,
Input,
Button,
notifications,
} from "@budibase/bbui"
import { onMount } from "svelte" import { onMount } from "svelte"
import { API } from "api" import { API } from "api"
import { notifications } from "@budibase/bbui"
import ErrorsBox from "components/common/ErrorsBox.svelte" import ErrorsBox from "components/common/ErrorsBox.svelte"
import { roles } from "stores/backend" import { roles } from "stores/backend"

View file

@ -1,5 +1,6 @@
<script> <script>
import { goto, isActive, params } from "@roxi/routify" import { goto, isActive, params } from "@roxi/routify"
import { Layout } from "@budibase/bbui"
import { BUDIBASE_INTERNAL_DB_ID } from "constants/backend" import { BUDIBASE_INTERNAL_DB_ID } from "constants/backend"
import { import {
database, database,
@ -21,8 +22,11 @@
import IntegrationIcon from "./IntegrationIcon.svelte" import IntegrationIcon from "./IntegrationIcon.svelte"
import { TableNames } from "constants" import { TableNames } from "constants"
import { userSelectedResourceMap } from "builderStore" import { userSelectedResourceMap } from "builderStore"
import { enrichDatasources } from "./datasourceUtils"
import { onMount } from "svelte"
let openDataSources = [] export let searchTerm
let toggledDatasources = {}
$: enrichedDataSources = enrichDatasources( $: enrichedDataSources = enrichDatasources(
$datasources, $datasources,
@ -32,52 +36,9 @@
$queries, $queries,
$views, $views,
$viewsV2, $viewsV2,
openDataSources toggledDatasources,
searchTerm
) )
$: openDataSource = enrichedDataSources.find(x => x.open)
$: {
// Ensure the open datasource is always actually open
if (openDataSource) {
openNode(openDataSource)
}
}
const enrichDatasources = (
datasources,
params,
isActive,
tables,
queries,
views,
viewsV2,
openDataSources
) => {
if (!datasources?.list?.length) {
return []
}
return datasources.list.map(datasource => {
const selected =
isActive("./datasource") &&
datasources.selectedDatasourceId === datasource._id
const open = openDataSources.includes(datasource._id)
const containsSelected = containsActiveEntity(
datasource,
params,
isActive,
tables,
queries,
views,
viewsV2
)
const onlySource = datasources.list.length === 1
return {
...datasource,
selected,
containsSelected,
open: selected || open || containsSelected || onlySource,
}
})
}
function selectDatasource(datasource) { function selectDatasource(datasource) {
openNode(datasource) openNode(datasource)
@ -91,102 +52,42 @@
} }
} }
function closeNode(datasource) {
openDataSources = openDataSources.filter(id => datasource._id !== id)
}
function openNode(datasource) { function openNode(datasource) {
if (!openDataSources.includes(datasource._id)) { toggledDatasources[datasource._id] = true
openDataSources = [...openDataSources, datasource._id]
}
} }
function toggleNode(datasource) { function toggleNode(datasource) {
const isOpen = openDataSources.includes(datasource._id) toggledDatasources[datasource._id] = !datasource.open
if (isOpen) {
closeNode(datasource)
} else {
openNode(datasource)
}
} }
const containsActiveEntity = ( const appUsersTableName = "App users"
datasource, $: showAppUsersTable =
params, !searchTerm ||
isActive, appUsersTableName.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1
tables,
queries,
views,
viewsV2
) => {
// Check for being on a datasource page
if (params.datasourceId === datasource._id) {
return true
}
// Check for hardcoded datasource edge cases onMount(() => {
if ( if ($tables.selected) {
isActive("./datasource/bb_internal") && toggledDatasources[$tables.selected.sourceId] = true
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 $: showNoResults =
if (params.queryId) { searchTerm && !showAppUsersTable && !enrichedDataSources.find(ds => ds.show)
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
}
</script> </script>
{#if $database?._id} {#if $database?._id}
<div class="hierarchy-items-container"> <div class="hierarchy-items-container">
<NavItem {#if showAppUsersTable}
icon="UserGroup" <NavItem
text="App users" icon="UserGroup"
selected={$isActive("./table/:tableId") && text={appUsersTableName}
$tables.selected?._id === TableNames.USERS} selected={$isActive("./table/:tableId") &&
on:click={() => selectTable(TableNames.USERS)} $tables.selected?._id === TableNames.USERS}
selectedBy={$userSelectedResourceMap[TableNames.USERS]} on:click={() => selectTable(TableNames.USERS)}
/> selectedBy={$userSelectedResourceMap[TableNames.USERS]}
{#each enrichedDataSources as datasource} />
{/if}
{#each enrichedDataSources.filter(ds => ds.show) as datasource}
<NavItem <NavItem
border border
text={datasource.name} text={datasource.name}
@ -210,8 +111,8 @@
</NavItem> </NavItem>
{#if datasource.open} {#if datasource.open}
<TableNavigator sourceId={datasource._id} {selectTable} /> <TableNavigator tables={datasource.tables} {selectTable} />
{#each $queries.list.filter(query => query.datasourceId === datasource._id) as query} {#each datasource.queries as query}
<NavItem <NavItem
indentLevel={1} indentLevel={1}
icon="SQLQuery" icon="SQLQuery"
@ -228,6 +129,13 @@
{/each} {/each}
{/if} {/if}
{/each} {/each}
{#if showNoResults}
<Layout paddingY="none" paddingX="L">
<div class="no-results">
There aren't any datasources matching that name
</div>
</Layout>
{/if}
</div> </div>
{/if} {/if}
@ -240,4 +148,8 @@
place-items: center; place-items: center;
flex: 0 0 24px; flex: 0 0 24px;
} }
.no-results {
color: var(--spectrum-global-color-gray-600);
}
</style> </style>

View file

@ -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,
}
})
}

View file

@ -1,8 +1,7 @@
<script> <script>
import { get } from "svelte/store" import { get } from "svelte/store"
import { datasources, integrations } from "stores/backend" import { datasources, integrations } from "stores/backend"
import { notifications } from "@budibase/bbui" import { notifications, Input, ModalContent, Modal } from "@budibase/bbui"
import { Input, ModalContent, Modal } from "@budibase/bbui"
import { integrationForDatasource } from "stores/selectors" import { integrationForDatasource } from "stores/selectors"
let error = "" let error = ""

View file

@ -1,8 +1,7 @@
<script> <script>
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { datasources } from "stores/backend" import { datasources } from "stores/backend"
import { notifications } from "@budibase/bbui" import { notifications, ActionMenu, MenuItem, Icon } from "@budibase/bbui"
import { ActionMenu, MenuItem, Icon } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import UpdateDatasourceModal from "components/backend/DatasourceNavigator/modals/UpdateDatasourceModal.svelte" import UpdateDatasourceModal from "components/backend/DatasourceNavigator/modals/UpdateDatasourceModal.svelte"
import { BUDIBASE_DATASOURCE_TYPE } from "constants/backend" import { BUDIBASE_DATASOURCE_TYPE } from "constants/backend"

View file

@ -0,0 +1,219 @@
import { enrichDatasources } from "../datasourceUtils"
describe("datasourceUtils", () => {
describe("enrichDatasources", () => {
it.each([
["undefined", undefined],
["undefined list", {}],
["empty list", { list: [] }],
])("%s datasources will return an empty list", datasources => {
const result = enrichDatasources(datasources)
expect(result).toEqual([])
})
describe("filtering", () => {
const internalTables = {
_id: "datasource_internal_bb_default",
name: "Sample Data",
}
const pgDatasource = {
_id: "pg_ds",
name: "PostgreSQL local",
}
const mysqlDatasource = {
_id: "mysql_ds",
name: "My SQL local",
}
const tables = [
...[
{
_id: "ta_bb_employee",
name: "Employees",
},
{
_id: "ta_bb_expenses",
name: "Expenses",
},
{
_id: "ta_bb_expenses_2",
name: "Expenses 2",
},
{
_id: "ta_bb_inventory",
name: "Inventory",
},
{
_id: "ta_bb_jobs",
name: "Jobs",
},
].map(t => ({
...t,
sourceId: internalTables._id,
})),
...[
{
_id: "pg_ds-external_inventory",
name: "External Inventory",
views: {
"External Inventory first view": {
name: "External Inventory first view",
id: "pg_ds_view_1",
},
"External Inventory second view": {
name: "External Inventory second view",
id: "pg_ds_view_2",
},
},
},
{
_id: "pg_ds-another_table",
name: "Another table",
views: {
view1: {
id: "pg_ds-another_table-view1",
name: "view1",
},
["View 2"]: {
id: "pg_ds-another_table-view2",
name: "View 2",
},
},
},
{
_id: "pg_ds_table2",
name: "table2",
views: {
"new 2": {
name: "new 2",
id: "pg_ds_table2_new_2",
},
new: {
name: "new",
id: "pg_ds_table2_new_",
},
},
},
].map(t => ({
...t,
sourceId: pgDatasource._id,
})),
...[
{
_id: "mysql_ds-mysql_table",
name: "MySQL table",
},
].map(t => ({
...t,
sourceId: mysqlDatasource._id,
})),
]
const datasources = {
list: [internalTables, pgDatasource, mysqlDatasource],
}
const isActive = vi.fn().mockReturnValue(true)
it("without a search term, all datasources are returned", () => {
const searchTerm = ""
const result = enrichDatasources(
datasources,
{},
isActive,
{ list: [] },
{ list: [] },
{ list: [] },
{ list: [] },
{},
searchTerm
)
expect(result).toEqual(
datasources.list.map(d =>
expect.objectContaining({
_id: d._id,
show: true,
})
)
)
})
it("given a valid search term, all tables are correctly filtered", () => {
const searchTerm = "ex"
const result = enrichDatasources(
datasources,
{},
isActive,
{ list: tables },
{ list: [] },
{ list: [] },
{ list: [] },
{},
searchTerm
)
expect(result).toEqual([
expect.objectContaining({
_id: internalTables._id,
show: true,
tables: [
expect.objectContaining({ _id: "ta_bb_expenses" }),
expect.objectContaining({ _id: "ta_bb_expenses_2" }),
],
}),
expect.objectContaining({
_id: pgDatasource._id,
show: true,
tables: [
expect.objectContaining({ _id: "pg_ds-external_inventory" }),
],
}),
expect.objectContaining({
_id: mysqlDatasource._id,
show: false,
tables: [],
}),
])
})
it("given a non matching search term, all entities are empty", () => {
const searchTerm = "non matching"
const result = enrichDatasources(
datasources,
{},
isActive,
{ list: tables },
{ list: [] },
{ list: [] },
{ list: [] },
{},
searchTerm
)
expect(result).toEqual([
expect.objectContaining({
_id: internalTables._id,
show: false,
tables: [],
}),
expect.objectContaining({
_id: pgDatasource._id,
show: false,
tables: [],
}),
expect.objectContaining({
_id: mysqlDatasource._id,
show: false,
tables: [],
}),
])
})
})
})
})

View file

@ -1,5 +1,8 @@
<script> <script>
import { RelationshipType } from "constants/backend" import {
RelationshipType,
PrettyRelationshipDefinitions,
} from "constants/backend"
import { import {
keepOpen, keepOpen,
Button, Button,
@ -8,13 +11,12 @@
Select, Select,
Detail, Detail,
Body, Body,
Helpers,
} from "@budibase/bbui" } from "@budibase/bbui"
import { tables } from "stores/backend" import { tables } from "stores/backend"
import { Helpers } from "@budibase/bbui"
import { RelationshipErrorChecker } from "./relationshipErrors" import { RelationshipErrorChecker } from "./relationshipErrors"
import { onMount } from "svelte" import { onMount } from "svelte"
import RelationshipSelector from "components/common/RelationshipSelector.svelte" import RelationshipSelector from "components/common/RelationshipSelector.svelte"
import { PrettyRelationshipDefinitions } from "constants/backend"
export let save export let save
export let datasource export let datasource

View file

@ -1,5 +1,10 @@
<script> <script>
import { tables, views, viewsV2, database } from "stores/backend" import {
tables as tablesStore,
views,
viewsV2,
database,
} from "stores/backend"
import { TableNames } from "constants" import { TableNames } from "constants"
import EditTablePopover from "./popovers/EditTablePopover.svelte" import EditTablePopover from "./popovers/EditTablePopover.svelte"
import EditViewPopover from "./popovers/EditViewPopover.svelte" import EditViewPopover from "./popovers/EditViewPopover.svelte"
@ -7,14 +12,10 @@
import { goto, isActive } from "@roxi/routify" import { goto, isActive } from "@roxi/routify"
import { userSelectedResourceMap } from "builderStore" import { userSelectedResourceMap } from "builderStore"
export let sourceId export let tables
export let selectTable export let selectTable
$: sortedTables = $tables.list $: sortedTables = tables.sort(alphabetical)
.filter(
table => table.sourceId === sourceId && table._id !== TableNames.USERS
)
.sort(alphabetical)
const alphabetical = (a, b) => { const alphabetical = (a, b) => {
return a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1 return a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1
@ -37,7 +38,7 @@
icon={table._id === TableNames.USERS ? "UserGroup" : "Table"} icon={table._id === TableNames.USERS ? "UserGroup" : "Table"}
text={table.name} text={table.name}
selected={$isActive("./table/:tableId") && selected={$isActive("./table/:tableId") &&
$tables.selected?._id === table._id} $tablesStore.selected?._id === table._id}
on:click={() => selectTable(table._id)} on:click={() => selectTable(table._id)}
selectedBy={$userSelectedResourceMap[table._id]} selectedBy={$userSelectedResourceMap[table._id]}
> >

View file

@ -1,9 +1,13 @@
<script> <script>
import { goto, url } from "@roxi/routify" import { goto, url } from "@roxi/routify"
import { tables } from "stores/backend" import { tables, datasources } from "stores/backend"
import { notifications } from "@budibase/bbui" import {
import { Input, Label, ModalContent, Layout } from "@budibase/bbui" notifications,
import { datasources } from "stores/backend" Input,
Label,
ModalContent,
Layout,
} from "@budibase/bbui"
import TableDataImport from "../TableDataImport.svelte" import TableDataImport from "../TableDataImport.svelte"
import { import {
BUDIBASE_INTERNAL_DB_ID, BUDIBASE_INTERNAL_DB_ID,

View file

@ -1,5 +1,6 @@
<script> <script>
import { Helpers } from "@budibase/bbui" import { Helpers } from "@budibase/bbui"
export let size export let size
export let svgHtml export let svgHtml

View file

@ -0,0 +1,149 @@
<script>
import { tick } from "svelte"
import { Icon, Body } from "@budibase/bbui"
import { keyUtils } from "helpers/keyUtils"
export let title
export let placeholder
export let value
export let onAdd
export let search
let searchInput
const openSearch = async () => {
search = true
await tick()
searchInput.focus()
}
const closeSearch = async () => {
search = false
value = ""
}
const onKeyDown = e => {
if (e.key === "Escape") {
closeSearch()
}
}
const handleAddButton = () => {
if (search) {
closeSearch()
} else {
onAdd()
}
}
</script>
<svelte:window on:keydown={onKeyDown} />
<div class="header" class:search>
<input
readonly={!search}
bind:value
bind:this={searchInput}
class="searchBox"
class:hide={!search}
{placeholder}
/>
<div class="title" class:hide={search}>
<Body size="S">{title}</Body>
</div>
<div
on:click={openSearch}
on:keydown={keyUtils.handleEnter(openSearch)}
class="searchButton"
class:hide={search}
>
<Icon size="S" name="Search" />
</div>
<div
on:click={handleAddButton}
on:keydown={keyUtils.handleEnter(handleAddButton)}
class="addButton"
class:rotate={search}
>
<Icon name="Add" />
</div>
</div>
<style>
.search {
transition: height 300ms ease-out;
max-height: none;
}
.header {
flex-shrink: 0;
flex-direction: row;
position: relative;
height: 50px;
width: 100%;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 2px solid transparent;
transition: border-bottom 130ms ease-out;
gap: var(--spacing-l);
}
.searchBox {
font-family: var(--font-sans);
color: var(--ink);
background-color: transparent;
border: none;
font-size: var(--spectrum-alias-font-size-default);
display: flex;
}
.searchBox:focus {
outline: none;
}
.searchBox::placeholder {
color: var(--spectrum-global-color-gray-600);
}
.title {
display: flex;
align-items: center;
height: 100%;
box-sizing: border-box;
flex: 1;
opacity: 1;
z-index: 1;
}
.searchButton {
color: var(--grey-7);
cursor: pointer;
opacity: 1;
}
.searchButton:hover {
color: var(--ink);
}
.hide {
opacity: 0;
pointer-events: none;
display: none !important;
}
.addButton {
display: flex;
transition: transform 300ms ease-out;
color: var(--grey-7);
cursor: pointer;
}
.addButton:hover {
color: var(--ink);
}
.rotate {
transform: rotate(45deg);
}
</style>

View file

@ -189,6 +189,7 @@
flex: 0 0 20px; flex: 0 0 20px;
pointer-events: all; pointer-events: all;
order: 0; order: 0;
transition: transform 100ms linear;
} }
.icon.arrow.absolute { .icon.arrow.absolute {
position: absolute; position: absolute;

View file

@ -20,73 +20,91 @@
export let allowedRoles = null export let allowedRoles = null
export let allowCreator = false export let allowCreator = false
export let fancySelect = false export let fancySelect = false
export let labelPrefix = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const RemoveID = "remove" const RemoveID = "remove"
$: enrichLabel = label => (labelPrefix ? `${labelPrefix} ${label}` : label)
$: options = getOptions( $: options = getOptions(
$roles, $roles,
allowPublic, allowPublic,
allowRemove, allowRemove,
allowedRoles, allowedRoles,
allowCreator allowCreator,
enrichLabel
) )
const getOptions = ( const getOptions = (
roles, roles,
allowPublic, allowPublic,
allowRemove, allowRemove,
allowedRoles, allowedRoles,
allowCreator allowCreator,
enrichLabel
) => { ) => {
// Use roles whitelist if specified
if (allowedRoles?.length) { if (allowedRoles?.length) {
const filteredRoles = roles.filter(role => let options = roles
allowedRoles.includes(role._id) .filter(role => allowedRoles.includes(role._id))
) .map(role => ({
return [ name: enrichLabel(role.name),
...filteredRoles, _id: role._id,
...(allowedRoles.includes(Constants.Roles.CREATOR) }))
? [{ _id: Constants.Roles.CREATOR, name: "Creator", enabled: false }] if (allowedRoles.includes(Constants.Roles.CREATOR)) {
: []), options.push({
]
}
let newRoles = [...roles]
if (allowCreator) {
newRoles = [
{
_id: Constants.Roles.CREATOR, _id: Constants.Roles.CREATOR,
name: "Creator", name: "Can edit",
tag: enabled: false,
!$licensing.perAppBuildersEnabled && })
capitalise(Constants.PlanType.BUSINESS), }
}, return options
...newRoles,
]
} }
// 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) { if (allowRemove) {
newRoles = [ options.push({
...newRoles, _id: RemoveID,
{ name: "Remove",
_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 => { 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 null
} }
return RoleUtils.getRoleColour(role._id) return RoleUtils.getRoleColour(role._id)
} }
const getIcon = role => { const getIcon = role => {
if (allowRemove && role._id === RemoveID) { // Only remove option has an icon
if (role._id === RemoveID) {
return "Close" return "Close"
} }
return null return null

View file

@ -9,18 +9,18 @@
Heading, Heading,
Icon, Icon,
} from "@budibase/bbui" } from "@budibase/bbui"
import { createEventDispatcher, onMount } from "svelte" import { createEventDispatcher, onMount, getContext } from "svelte"
import { import {
isValid, isValid,
decodeJSBinding, decodeJSBinding,
encodeJSBinding, encodeJSBinding,
convertToJS,
} from "@budibase/string-templates" } from "@budibase/string-templates"
import { import {
readableToRuntimeBinding, readableToRuntimeBinding,
runtimeToReadableBinding, runtimeToReadableBinding,
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
import { convertToJS } from "@budibase/string-templates"
import { admin } from "stores/portal" import { admin } from "stores/portal"
import CodeEditor from "../CodeEditor/CodeEditor.svelte" import CodeEditor from "../CodeEditor/CodeEditor.svelte"
import { import {
@ -32,7 +32,6 @@
hbInsert, hbInsert,
jsInsert, jsInsert,
} from "../CodeEditor" } from "../CodeEditor"
import { getContext } from "svelte"
import BindingPicker from "./BindingPicker.svelte" import BindingPicker from "./BindingPicker.svelte"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()

View file

@ -1,5 +1,6 @@
<script> <script>
import { capitalise } from "helpers" import { capitalise } from "helpers"
export let value export let value
</script> </script>

View file

@ -1,5 +1,6 @@
<script> <script>
import dayjs from "dayjs" import dayjs from "dayjs"
export let value export let value
</script> </script>

View file

@ -20,7 +20,12 @@
import analytics, { Events, EventSource } from "analytics" import analytics, { Events, EventSource } from "analytics"
import { API } from "api" import { API } from "api"
import { apps } from "stores/portal" 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 TourWrap from "components/portal/onboarding/TourWrap.svelte"
import { TOUR_STEP_KEYS } from "components/portal/onboarding/tours.js" import { TOUR_STEP_KEYS } from "components/portal/onboarding/tours.js"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
@ -48,7 +53,7 @@
$store.upgradableVersion && $store.upgradableVersion &&
$store.version && $store.version &&
$store.upgradableVersion !== $store.version $store.upgradableVersion !== $store.version
$: canPublish = !publishing && loaded $: canPublish = !publishing && loaded && $sortedScreens.length > 0
$: lastDeployed = getLastDeployedString($deploymentStore) $: lastDeployed = getLastDeployedString($deploymentStore)
const initialiseApp = async () => { const initialiseApp = async () => {
@ -175,7 +180,12 @@
<div class="app-action-button preview"> <div class="app-action-button preview">
<div class="app-action"> <div class="app-action">
<ActionButton quiet icon="PlayCircle" on:click={previewApp}> <ActionButton
disabled={$sortedScreens.length === 0}
quiet
icon="PlayCircle"
on:click={previewApp}
>
Preview Preview
</ActionButton> </ActionButton>
</div> </div>

View file

@ -11,6 +11,7 @@
export let onClickCloseButton export let onClickCloseButton
export let borderLeft = false export let borderLeft = false
export let borderRight = false export let borderRight = false
export let borderBottomHeader = true
export let wide = false export let wide = false
export let extraWide = false export let extraWide = false
export let closeButtonIcon = "Close" export let closeButtonIcon = "Close"
@ -27,7 +28,12 @@
class:borderLeft class:borderLeft
class:borderRight class:borderRight
> >
<div class="header" class:noHeaderBorder class:custom={customHeaderContent}> <div
class="header"
class:custom={customHeaderContent}
class:borderBottom={borderBottomHeader}
class:noHeaderBorder
>
{#if showBackButton} {#if showBackButton}
<Icon name="ArrowLeft" hoverable on:click={onClickBackButton} /> <Icon name="ArrowLeft" hoverable on:click={onClickBackButton} />
{/if} {/if}
@ -95,13 +101,15 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 0 var(--spacing-l); padding: 0 var(--spacing-l);
border-bottom: var(--border-light);
gap: var(--spacing-m); gap: var(--spacing-m);
} }
.noHeaderBorder { .noHeaderBorder {
border-bottom: none !important; border-bottom: none !important;
} }
.header.borderBottom {
border-bottom: var(--border-light);
}
.title { .title {
flex: 1 1 auto; flex: 1 1 auto;
width: 0; width: 0;

View file

@ -20,7 +20,7 @@ import ValidationEditor from "./controls/ValidationEditor/ValidationEditor.svelt
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import ColumnEditor from "./controls/ColumnEditor/ColumnEditor.svelte" import ColumnEditor from "./controls/ColumnEditor/ColumnEditor.svelte"
import BasicColumnEditor from "./controls/ColumnEditor/BasicColumnEditor.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 BarButtonList from "./controls/BarButtonList.svelte"
import FieldConfiguration from "./controls/FieldConfiguration/FieldConfiguration.svelte" import FieldConfiguration from "./controls/FieldConfiguration/FieldConfiguration.svelte"
import ButtonConfiguration from "./controls/ButtonConfiguration/ButtonConfiguration.svelte" import ButtonConfiguration from "./controls/ButtonConfiguration/ButtonConfiguration.svelte"
@ -28,6 +28,7 @@ import RelationshipFilterEditor from "./controls/RelationshipFilterEditor.svelte
const componentMap = { const componentMap = {
text: DrawerBindableInput, text: DrawerBindableInput,
plainText: Input,
select: Select, select: Select,
radio: RadioGroup, radio: RadioGroup,
dataSource: DataSourceSelect, dataSource: DataSourceSelect,

View file

@ -1,7 +1,6 @@
<script> <script>
import { ActionButton, Button, Drawer } from "@budibase/bbui" import { ActionButton, Button, Drawer, notifications } from "@budibase/bbui"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { notifications } from "@budibase/bbui"
import ButtonActionDrawer from "./ButtonActionDrawer.svelte" import ButtonActionDrawer from "./ButtonActionDrawer.svelte"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"

View file

@ -1,17 +1,19 @@
<script> <script>
import { Select, Label } from "@budibase/bbui" import { Select, Label } from "@budibase/bbui"
import { currentAsset, store } from "builderStore" import { currentAsset, store } from "builderStore"
import { getActionProviderComponents } from "builderStore/dataBinding" import { getActionProviders } from "builderStore/dataBinding"
import { onMount } from "svelte" import { onMount } from "svelte"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
export let parameters export let parameters
export let bindings = [] export let bindings = []
export let nested
$: actionProviders = getActionProviderComponents( $: actionProviders = getActionProviders(
$currentAsset, $currentAsset,
$store.selectedComponentId, $store.selectedComponentId,
"ChangeFormStep" "ChangeFormStep",
{ includeSelf: nested }
) )
const typeOptions = [ const typeOptions = [
@ -46,8 +48,8 @@
placeholder={null} placeholder={null}
bind:value={parameters.componentId} bind:value={parameters.componentId}
options={actionProviders} options={actionProviders}
getOptionLabel={x => x._instanceName} getOptionLabel={x => x.readableBinding}
getOptionValue={x => x._id} getOptionValue={x => x.runtimeBinding}
/> />
<Label small>Step</Label> <Label small>Step</Label>
<Select bind:value={parameters.type} options={typeOptions} /> <Select bind:value={parameters.type} options={typeOptions} />

View file

@ -1,14 +1,16 @@
<script> <script>
import { Select, Label } from "@budibase/bbui" import { Select, Label } from "@budibase/bbui"
import { currentAsset, store } from "builderStore" import { currentAsset, store } from "builderStore"
import { getActionProviderComponents } from "builderStore/dataBinding" import { getActionProviders } from "builderStore/dataBinding"
export let parameters export let parameters
export let nested
$: actionProviders = getActionProviderComponents( $: actionProviders = getActionProviders(
$currentAsset, $currentAsset,
$store.selectedComponentId, $store.selectedComponentId,
"ClearForm" "ClearForm",
{ includeSelf: nested }
) )
</script> </script>
@ -17,8 +19,8 @@
<Select <Select
bind:value={parameters.componentId} bind:value={parameters.componentId}
options={actionProviders} options={actionProviders}
getOptionLabel={x => x._instanceName} getOptionLabel={x => x.readableBinding}
getOptionValue={x => x._id} getOptionValue={x => x.runtimeBinding}
/> />
</div> </div>

View file

@ -2,6 +2,7 @@
import { Select, Body } from "@budibase/bbui" import { Select, Body } from "@budibase/bbui"
import { onMount } from "svelte" import { onMount } from "svelte"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
export let parameters export let parameters
export let bindings export let bindings

View file

@ -2,27 +2,20 @@
import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui" import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui"
import { store, currentAsset } from "builderStore" import { store, currentAsset } from "builderStore"
import { tables, viewsV2 } from "stores/backend" import { tables, viewsV2 } from "stores/backend"
import { import { getSchemaForDatasourcePlus } from "builderStore/dataBinding"
getContextProviderComponents,
getSchemaForDatasourcePlus,
} from "builderStore/dataBinding"
import SaveFields from "./SaveFields.svelte" import SaveFields from "./SaveFields.svelte"
import { getDatasourceLikeProviders } from "components/design/settings/controls/ButtonActionEditor/actions/utils"
export let parameters export let parameters
export let bindings = [] export let bindings = []
export let nested
$: formComponents = getContextProviderComponents( $: providerOptions = getDatasourceLikeProviders({
$currentAsset, asset: $currentAsset,
$store.selectedComponentId, componentId: $store.selectedComponentId,
"form" nested,
) })
$: schemaComponents = getContextProviderComponents( $: schemaFields = getSchemaFields(parameters?.tableId)
$currentAsset,
$store.selectedComponentId,
"schema"
)
$: providerOptions = getProviderOptions(formComponents, schemaComponents)
$: schemaFields = getSchemaFields($currentAsset, parameters?.tableId)
$: tableOptions = $tables.list.map(table => ({ $: tableOptions = $tables.list.map(table => ({
label: table.name, label: table.name,
resourceId: table._id, resourceId: table._id,
@ -33,44 +26,8 @@
})) }))
$: options = [...(tableOptions || []), ...(viewOptions || [])] $: options = [...(tableOptions || []), ...(viewOptions || [])]
// Gets a context definition of a certain type from a component definition const getSchemaFields = resourceId => {
const extractComponentContext = (component, contextType) => { const { schema } = getSchemaForDatasourcePlus(resourceId)
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 = (asset, tableId) => {
const { schema } = getSchemaForDatasourcePlus(tableId)
delete schema._id
delete schema._rev
return Object.values(schema || {}) return Object.values(schema || {})
} }

View file

@ -1,14 +1,16 @@
<script> <script>
import { Select, Label } from "@budibase/bbui" import { Select, Label } from "@budibase/bbui"
import { currentAsset, store } from "builderStore" import { currentAsset, store } from "builderStore"
import { getActionProviderComponents } from "builderStore/dataBinding" import { getActionProviders } from "builderStore/dataBinding"
export let parameters export let parameters
export let nested
$: actionProviders = getActionProviderComponents( $: actionProviders = getActionProviders(
$currentAsset, $currentAsset,
$store.selectedComponentId, $store.selectedComponentId,
"RefreshDatasource" "RefreshDatasource",
{ includeSelf: nested }
) )
</script> </script>
@ -17,8 +19,8 @@
<Select <Select
bind:value={parameters.componentId} bind:value={parameters.componentId}
options={actionProviders} options={actionProviders}
getOptionLabel={x => x._instanceName} getOptionLabel={x => x.readableBinding}
getOptionValue={x => x._id} getOptionValue={x => x.runtimeBinding}
/> />
</div> </div>

View file

@ -2,29 +2,19 @@
import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui" import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui"
import { store, currentAsset } from "builderStore" import { store, currentAsset } from "builderStore"
import { tables, viewsV2 } from "stores/backend" import { tables, viewsV2 } from "stores/backend"
import { import { getSchemaForDatasourcePlus } from "builderStore/dataBinding"
getContextProviderComponents,
getSchemaForDatasourcePlus,
} from "builderStore/dataBinding"
import SaveFields from "./SaveFields.svelte" import SaveFields from "./SaveFields.svelte"
import { getDatasourceLikeProviders } from "components/design/settings/controls/ButtonActionEditor/actions/utils"
export let parameters export let parameters
export let bindings = [] export let bindings = []
export let nested export let nested
$: formComponents = getContextProviderComponents( $: providerOptions = getDatasourceLikeProviders({
$currentAsset, asset: $currentAsset,
$store.selectedComponentId, componentId: $store.selectedComponentId,
"form", nested,
{ includeSelf: nested } })
)
$: schemaComponents = getContextProviderComponents(
$currentAsset,
$store.selectedComponentId,
"schema",
{ includeSelf: nested }
)
$: providerOptions = getProviderOptions(formComponents, schemaComponents)
$: schemaFields = getSchemaFields(parameters?.tableId) $: schemaFields = getSchemaFields(parameters?.tableId)
$: tableOptions = $tables.list.map(table => ({ $: tableOptions = $tables.list.map(table => ({
label: table.name, label: table.name,
@ -36,40 +26,6 @@
})) }))
$: options = [...(tableOptions || []), ...(viewOptions || [])] $: 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 getSchemaFields = resourceId => {
const { schema } = getSchemaForDatasourcePlus(resourceId) const { schema } = getSchemaForDatasourcePlus(resourceId)
return Object.values(schema || {}) return Object.values(schema || {})

View file

@ -1,22 +1,36 @@
<script> <script>
import { currentAsset, store } from "builderStore" import { currentAsset, store } from "builderStore"
import { Label, Combobox, Select } from "@budibase/bbui" import { Label, Combobox, Select } from "@budibase/bbui"
import { import { getActionProviders, buildFormSchema } from "builderStore/dataBinding"
getActionProviderComponents,
buildFormSchema,
} from "builderStore/dataBinding"
import { findComponent } from "builderStore/componentUtils" import { findComponent } from "builderStore/componentUtils"
export let parameters export let parameters
export let nested
$: formComponent = findComponent($currentAsset.props, parameters.componentId) $: formComponent = getFormComponent(
$currentAsset.props,
parameters.componentId
)
$: formSchema = buildFormSchema(formComponent) $: formSchema = buildFormSchema(formComponent)
$: fieldOptions = Object.keys(formSchema || {}) $: fieldOptions = Object.keys(formSchema || {})
$: actionProviders = getActionProviderComponents( $: actionProviders = getActionProviders(
$currentAsset, $currentAsset,
$store.selectedComponentId, $store.selectedComponentId,
"ScrollTo" "ScrollTo",
{ includeSelf: nested }
) )
const getFormComponent = (asset, id) => {
let component = findComponent(asset, id)
if (component) {
return component
}
// Check for block component IDs, and use the block itself instead
if (id?.includes("-")) {
return findComponent(asset, id.split("-")[0])
}
return null
}
</script> </script>
<div class="root"> <div class="root">
@ -24,8 +38,8 @@
<Select <Select
bind:value={parameters.componentId} bind:value={parameters.componentId}
options={actionProviders} options={actionProviders}
getOptionLabel={x => x._instanceName} getOptionLabel={x => x.readableBinding}
getOptionValue={x => x._id} getOptionValue={x => x.runtimeBinding}
/> />
<Label small>Field</Label> <Label small>Field</Label>
<Combobox bind:value={parameters.field} options={fieldOptions} /> <Combobox bind:value={parameters.field} options={fieldOptions} />

Some files were not shown because too many files have changed in this diff Show more