1
0
Fork 0
mirror of synced 2024-07-14 18:55:45 +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": "^_"
}
],
"import/no-relative-packages": "error"
"import/no-relative-packages": "error",
"import/export": "error",
"import/no-duplicates": "error",
"import/newline-after-import": "error"
},
"globals": {
"GeolocationPositionError": true

View file

@ -29,7 +29,6 @@ WORKDIR /opt/couchdb
ADD couch/vm.args couch/local.ini ./etc/
WORKDIR /
ADD build-target-paths.sh .
ADD runner.sh ./bbcouch-runner.sh
RUN chmod +x ./bbcouch-runner.sh /opt/clouseau/bin/clouseau ./build-target-paths.sh
RUN chmod +x ./bbcouch-runner.sh /opt/clouseau/bin/clouseau
CMD ["./bbcouch-runner.sh"]

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
DATA_DIR=${DATA_DIR:-/data}
mkdir -p ${DATA_DIR}
mkdir -p ${DATA_DIR}/couch/{dbs,views}
mkdir -p ${DATA_DIR}/search
chown -R couchdb:couchdb ${DATA_DIR}/couch
/build-target-paths.sh
echo ${TARGETBUILD} > /buildtarget.txt
if [[ "${TARGETBUILD}" = "aas" ]]; then
# Azure AppService uses /home for persistent data & SSH on port 2222
DATA_DIR="${DATA_DIR:-/home}"
WEBSITES_ENABLE_APP_SERVICE_STORAGE=true
mkdir -p $DATA_DIR/{search,minio,couch}
mkdir -p $DATA_DIR/couch/{dbs,views}
chown -R couchdb:couchdb $DATA_DIR/couch/
apt update
apt-get install -y openssh-server
echo "root:Docker!" | chpasswd
mkdir -p /tmp
chmod +x /tmp/ssh_setup.sh \
&& (sleep 1;/tmp/ssh_setup.sh 2>&1 > /dev/null)
cp /etc/sshd_config /etc/ssh/sshd_config
/etc/init.d/ssh restart
sed -i "s#DATA_DIR#/home#g" /opt/clouseau/clouseau.ini
sed -i "s#DATA_DIR#/home#g" /opt/couchdb/etc/local.ini
elif [[ "${TARGETBUILD}" = "single" ]]; then
sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini
sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini
elif [[ -n $KUBERNETES_SERVICE_HOST ]]; then
# In Kubernetes the directory /opt/couchdb/data has a persistent volume
# mount for storing database data.
sed -i "s#DATA_DIR#/opt/couchdb/data#g" /opt/clouseau/clouseau.ini
sed -i "s#DATA_DIR#/opt/couchdb/data#g" /opt/couchdb/etc/local.ini
sed -i "s/^-name .*$//g" /opt/couchdb/etc/vm.args
else
sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini
sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini
fi
/opt/clouseau/bin/clouseau > /dev/stdout 2>&1 &
/docker-entrypoint.sh /opt/couchdb/bin/couchdb &
sleep 10
while [[ $(curl -s -w "%{http_code}\n" http://localhost:5984/_up -o /dev/null) -ne 200 ]]; do
echo 'Waiting for CouchDB to start...';
sleep 5;
done
curl -X PUT http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984/_users
curl -X PUT http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984/_replicator
sleep infinity

View file

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

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
# install node-gyp dependencies
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends apt-utils cron g++ make python3
RUN apt-get update && apt-get install -y --no-install-recommends g++ make python3 jq
# add pin script
WORKDIR /
ADD scripts/cleanup.sh ./
RUN chmod +x /cleanup.sh
# build server
# copy and install dependencies
WORKDIR /app
ADD packages/server .
COPY package.json .
COPY yarn.lock .
RUN yarn install --production=true --network-timeout 1000000
RUN /cleanup.sh
COPY lerna.json .
COPY .yarnrc .
# build worker
WORKDIR /worker
ADD packages/worker .
COPY yarn.lock .
RUN yarn install --production=true --network-timeout 1000000
RUN /cleanup.sh
COPY packages/server/package.json packages/server/package.json
COPY packages/worker/package.json packages/worker/package.json
# string-templates does not get bundled during the esbuild process, so we want to use the local version
COPY packages/string-templates/package.json packages/string-templates/package.json
FROM budibase/couchdb
COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.sh
RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
RUN ./scripts/removeWorkspaceDependencies.sh packages/server/package.json
RUN ./scripts/removeWorkspaceDependencies.sh packages/worker/package.json
# We will never want to sync pro, but the script is still required
RUN echo '' > scripts/syncProPackage.js
RUN jq 'del(.scripts.postinstall)' package.json > temp.json && mv temp.json package.json
RUN ./scripts/removeWorkspaceDependencies.sh package.json
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production
# copy the actual code
COPY packages/server/dist packages/server/dist
COPY packages/server/pm2.config.js packages/server/pm2.config.js
COPY packages/server/client packages/server/client
COPY packages/server/builder packages/server/builder
COPY packages/worker/dist packages/worker/dist
COPY packages/worker/pm2.config.js packages/worker/pm2.config.js
COPY packages/string-templates packages/string-templates
FROM budibase/couchdb as runner
ARG TARGETARCH
ENV TARGETARCH $TARGETARCH
ENV NODE_MAJOR 18
#TARGETBUILD can be set to single (for single docker image) or aas (for azure app service)
# e.g. docker build --build-arg TARGETBUILD=aas ....
ARG TARGETBUILD=single
ENV TARGETBUILD $TARGETBUILD
COPY --from=build /app /app
COPY --from=build /worker /worker
# install base dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends software-properties-common nginx uuid-runtime redis-server
apt-get install -y --no-install-recommends software-properties-common nginx uuid-runtime redis-server libaio1
# Install postgres client for pg_dump utils
RUN apt install software-properties-common apt-transport-https gpg -y \
RUN apt install -y software-properties-common apt-transport-https ca-certificates gnupg \
&& curl -fsSl https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null \
&& echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main | tee /etc/apt/sources.list.d/postgresql.list \
&& apt update -y \
@ -47,14 +62,12 @@ RUN apt install software-properties-common apt-transport-https gpg -y \
# install other dependencies, nodejs, oracle requirements, jdk8, redis, nginx
WORKDIR /nodejs
RUN curl -sL https://deb.nodesource.com/setup_16.x -o /tmp/nodesource_setup.sh && \
bash /tmp/nodesource_setup.sh && \
apt-get install -y --no-install-recommends libaio1 nodejs && \
npm install --global yarn pm2
COPY scripts/install-node.sh ./install.sh
RUN chmod +x install.sh && ./install.sh
# setup nginx
ADD hosting/single/nginx/nginx.conf /etc/nginx
ADD hosting/single/nginx/nginx-default-site.conf /etc/nginx/sites-enabled/default
COPY hosting/single/nginx/nginx.conf /etc/nginx
COPY hosting/single/nginx/nginx-default-site.conf /etc/nginx/sites-enabled/default
RUN mkdir -p /var/log/nginx && \
touch /var/log/nginx/error.log && \
touch /var/run/nginx.pid && \
@ -62,29 +75,39 @@ RUN mkdir -p /var/log/nginx && \
WORKDIR /
RUN mkdir -p scripts/integrations/oracle
ADD packages/server/scripts/integrations/oracle scripts/integrations/oracle
COPY packages/server/scripts/integrations/oracle scripts/integrations/oracle
RUN /bin/bash -e ./scripts/integrations/oracle/instantclient/linux/install.sh
# setup minio
WORKDIR /minio
ADD scripts/install-minio.sh ./install.sh
COPY scripts/install-minio.sh ./install.sh
RUN chmod +x install.sh && ./install.sh
# setup runner file
WORKDIR /
ADD hosting/single/runner.sh .
COPY hosting/single/runner.sh .
RUN chmod +x ./runner.sh
ADD hosting/single/healthcheck.sh .
COPY hosting/single/healthcheck.sh .
RUN chmod +x ./healthcheck.sh
# Script below sets the path for storing data based on $DATA_DIR
# For Azure App Service install SSH & point data locations to /home
ADD hosting/single/ssh/sshd_config /etc/
ADD hosting/single/ssh/ssh_setup.sh /tmp
RUN /build-target-paths.sh
COPY hosting/single/ssh/sshd_config /etc/
COPY hosting/single/ssh/ssh_setup.sh /tmp
# setup letsencrypt certificate
RUN apt-get install -y certbot python3-certbot-nginx
COPY hosting/letsencrypt /app/letsencrypt
RUN chmod +x /app/letsencrypt/certificate-request.sh /app/letsencrypt/certificate-renew.sh
COPY --from=build /app/node_modules /node_modules
COPY --from=build /app/package.json /package.json
COPY --from=build /app/packages/server /app
COPY --from=build /app/packages/worker /worker
COPY --from=build /app/packages/string-templates /string-templates
RUN cd /string-templates && yarn link && cd ../app && yarn link @budibase/string-templates && cd ../worker && yarn link @budibase/string-templates
# cleanup cache
RUN yarn cache clean -f
EXPOSE 80
EXPOSE 443
@ -92,20 +115,10 @@ EXPOSE 443
EXPOSE 2222
VOLUME /data
# setup letsencrypt certificate
RUN apt-get install -y certbot python3-certbot-nginx
ADD hosting/letsencrypt /app/letsencrypt
RUN chmod +x /app/letsencrypt/certificate-request.sh /app/letsencrypt/certificate-renew.sh
# Remove cached files
RUN rm -rf \
/root/.cache \
/root/.npm \
/root/.pip \
/usr/local/share/doc \
/usr/share/doc \
/usr/share/man \
/var/lib/apt/lists/* \
/tmp/*
ARG BUDIBASE_VERSION
# Ensuring the version argument is sent
RUN test -n "$BUDIBASE_VERSION"
ENV BUDIBASE_VERSION=$BUDIBASE_VERSION
HEALTHCHECK --interval=15s --timeout=15s --start-period=45s CMD "/healthcheck.sh"

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
fi
if [[ $(curl -s -w "%{http_code}\n" http://localhost:5984/ -o /dev/null) -ne 200 ]]; then
if [[ $(curl -s -w "%{http_code}\n" http://localhost:5984/_up -o /dev/null) -ne 200 ]]; then
echo 'ERROR: CouchDB is not running';
healthy=false
fi

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,12 @@
import { BuiltinPermissionID, PermissionLevel } from "./permissions"
import { prefixRoleID, getRoleParams, DocumentType, SEPARATOR } from "../db"
import {
prefixRoleID,
getRoleParams,
DocumentType,
SEPARATOR,
doWithDB,
} from "../db"
import { getAppDB } from "../context"
import { doWithDB } from "../db"
import { Screen, Role as RoleDoc } from "@budibase/types"
import cloneDeep from "lodash/fp/cloneDeep"

View file

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

View file

@ -1,9 +1,8 @@
import env from "../environment"
import * as eventHelpers from "./events"
import * as accounts from "../accounts"
import * as accountSdk from "../accounts"
import * as cache from "../cache"
import { getGlobalDB, getIdentity, getTenantId } from "../context"
import { doInTenant, getGlobalDB, getIdentity, getTenantId } from "../context"
import * as dbUtils from "../db"
import { EmailUnavailableError, HTTPError } from "../errors"
import * as platform from "../platform"
@ -11,12 +10,10 @@ import * as sessions from "../security/sessions"
import * as usersCore from "./users"
import {
Account,
AllDocsResponse,
BulkUserCreated,
BulkUserDeleted,
isSSOAccount,
isSSOUser,
RowResponse,
SaveUserOpts,
User,
UserStatus,
@ -149,12 +146,12 @@ export class UserDB {
static async allUsers() {
const db = getGlobalDB()
const response = await db.allDocs(
const response = await db.allDocs<User>(
dbUtils.getGlobalUserParams(null, {
include_docs: true,
})
)
return response.rows.map((row: any) => row.doc)
return response.rows.map(row => row.doc!)
}
static async countUsersByApp(appId: string) {
@ -212,13 +209,6 @@ export class UserDB {
throw new Error("_id or email is required")
}
if (
user.builder?.apps?.length &&
!(await UserDB.features.isAppBuildersEnabled())
) {
throw new Error("Unable to update app builders, please check license")
}
let dbUser: User | undefined
if (_id) {
// try to get existing user from db
@ -467,7 +457,7 @@ export class UserDB {
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
// root account holder can't be deleted from inside budibase
const email = dbUser.email
const account = await accounts.getAccount(email)
const account = await accountSdk.getAccount(email)
if (account) {
if (dbUser.userId === getIdentity()!._id) {
throw new HTTPError('Please visit "Account" to delete this user', 400)
@ -488,6 +478,37 @@ export class UserDB {
await sessions.invalidateSessions(userId, { reason: "deletion" })
}
static async createAdminUser(
email: string,
password: string,
tenantId: string,
opts?: { ssoId?: string; hashPassword?: boolean; requirePassword?: boolean }
) {
const user: User = {
email: email,
password: password,
createdAt: Date.now(),
roles: {},
builder: {
global: true,
},
admin: {
global: true,
},
tenantId,
}
if (opts?.ssoId) {
user.ssoId = opts.ssoId
}
// always bust checklist beforehand, if an error occurs but can proceed, don't get
// stuck in a cycle
await cache.bustCache(cache.CacheKey.CHECKLIST)
return await UserDB.save(user, {
hashPassword: opts?.hashPassword,
requirePassword: opts?.requirePassword,
})
}
static async getGroups(groupIds: string[]) {
return await this.groups.getBulk(groupIds)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,2 +1,3 @@
import Chance from "./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
// use tk.reset() to use real dates in individual tests
import tk from "timekeeper"
tk.freeze(mocks.date.MOCK_DATE)
if (!process.env.DEBUG) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,7 +17,7 @@
export let options = []
export let getOptionLabel = option => extractProperty(option, "label")
export let getOptionValue = option => extractProperty(option, "value")
export let getOptionSubtitle = option => option?.subtitle
export let isOptionSelected = () => false
const dispatch = createEventDispatcher()
@ -135,7 +135,7 @@
class="spectrum-Textfield-input spectrum-InputGroup-input"
/>
</div>
<div style="width: 30%">
<div style="width: 40%">
<button
{id}
class="spectrum-Picker spectrum-Picker--sizeM override-borders"
@ -157,38 +157,43 @@
<use xlink:href="#spectrum-css-icon-Chevron100" />
</svg>
</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>
{#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>
<style>
@ -196,7 +201,6 @@
min-width: 0;
width: 100%;
}
.spectrum-InputGroup-input {
border-right-width: 1px;
}
@ -206,7 +210,6 @@
.spectrum-Textfield-input {
width: 0;
}
.override-borders {
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
@ -215,5 +218,18 @@
max-height: 240px;
z-index: 999;
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>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -228,7 +228,12 @@ export const getContextProviderComponents = (
/**
* 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) {
return []
}
@ -236,13 +241,30 @@ export const getActionProviderComponents = (asset, componentId, actionType) => {
// Get the component tree leading up to this component, ignoring the component
// itself
const path = findComponentPath(asset.props, componentId)
path.pop()
if (!options?.includeSelf) {
path.pop()
}
// Filter by only data provider components
return path.filter(component => {
// Find matching contexts and generate bindings
let providers = []
path.forEach(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,
* this makes sure that doesn't happen.
*/
const shouldReplaceBinding = (currentValue, convertFrom, convertTo) => {
if (!currentValue?.includes(convertFrom)) {
const shouldReplaceBinding = (currentValue, from, convertTo, binding) => {
if (!currentValue?.includes(from)) {
return false
}
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
// this makes sure it is detected
const noSpaces = currentValue.replace(/\s+/g, "")
const fromNoSpaces = convertFrom.replace(/\s+/g, "")
const fromNoSpaces = from.replace(/\s+/g, "")
const invalids = [
`[${fromNoSpaces}]`,
`"${fromNoSpaces}"`,
@ -1130,8 +1153,11 @@ const bindingReplacement = (
// in the search, working from longest to shortest so always use best match first
let searchString = newBoundValue
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
do {
// 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 { getUserStore } from "./store/users"
import { getDeploymentStore } from "./store/deployments"
import { derived, writable } from "svelte/store"
import { derived, writable, get } from "svelte/store"
import { findComponent, findComponentPath } from "./componentUtils"
import { RoleUtils } from "@budibase/frontend-core"
import { createHistoryStore } from "builderStore/store/history"
import { get } from "svelte/store"
export const store = getFrontendStore()
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"
import { datasources, tables } from "stores/backend"
import { get } from "svelte/store"
import { auth } from "stores/portal"
import { SocketEvent, BuilderSocketEvent } from "@budibase/shared-core"
import { apps } from "stores/portal"
import { auth, apps } from "stores/portal"
import { SocketEvent, BuilderSocketEvent, helpers } from "@budibase/shared-core"
import { notifications } from "@budibase/bbui"
import { helpers } from "@budibase/shared-core"
export const createBuilderWebsocket = appId => {
const socket = createWebsocket("/socket/builder")

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,14 @@
<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 { API } from "api"
import { notifications } from "@budibase/bbui"
import ErrorsBox from "components/common/ErrorsBox.svelte"
import { roles } from "stores/backend"

View file

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

View file

@ -1,8 +1,7 @@
<script>
import { goto } from "@roxi/routify"
import { datasources } from "stores/backend"
import { notifications } from "@budibase/bbui"
import { ActionMenu, MenuItem, Icon } from "@budibase/bbui"
import { notifications, ActionMenu, MenuItem, Icon } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import UpdateDatasourceModal from "components/backend/DatasourceNavigator/modals/UpdateDatasourceModal.svelte"
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>
import { RelationshipType } from "constants/backend"
import {
RelationshipType,
PrettyRelationshipDefinitions,
} from "constants/backend"
import {
keepOpen,
Button,
@ -8,13 +11,12 @@
Select,
Detail,
Body,
Helpers,
} from "@budibase/bbui"
import { tables } from "stores/backend"
import { Helpers } from "@budibase/bbui"
import { RelationshipErrorChecker } from "./relationshipErrors"
import { onMount } from "svelte"
import RelationshipSelector from "components/common/RelationshipSelector.svelte"
import { PrettyRelationshipDefinitions } from "constants/backend"
export let save
export let datasource

View file

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

View file

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

View file

@ -1,5 +1,6 @@
<script>
import { Helpers } from "@budibase/bbui"
export let size
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;
pointer-events: all;
order: 0;
transition: transform 100ms linear;
}
.icon.arrow.absolute {
position: absolute;

View file

@ -20,73 +20,91 @@
export let allowedRoles = null
export let allowCreator = false
export let fancySelect = false
export let labelPrefix = null
const dispatch = createEventDispatcher()
const RemoveID = "remove"
$: enrichLabel = label => (labelPrefix ? `${labelPrefix} ${label}` : label)
$: options = getOptions(
$roles,
allowPublic,
allowRemove,
allowedRoles,
allowCreator
allowCreator,
enrichLabel
)
const getOptions = (
roles,
allowPublic,
allowRemove,
allowedRoles,
allowCreator
allowCreator,
enrichLabel
) => {
// Use roles whitelist if specified
if (allowedRoles?.length) {
const filteredRoles = roles.filter(role =>
allowedRoles.includes(role._id)
)
return [
...filteredRoles,
...(allowedRoles.includes(Constants.Roles.CREATOR)
? [{ _id: Constants.Roles.CREATOR, name: "Creator", enabled: false }]
: []),
]
}
let newRoles = [...roles]
if (allowCreator) {
newRoles = [
{
let options = roles
.filter(role => allowedRoles.includes(role._id))
.map(role => ({
name: enrichLabel(role.name),
_id: role._id,
}))
if (allowedRoles.includes(Constants.Roles.CREATOR)) {
options.push({
_id: Constants.Roles.CREATOR,
name: "Creator",
tag:
!$licensing.perAppBuildersEnabled &&
capitalise(Constants.PlanType.BUSINESS),
},
...newRoles,
]
name: "Can edit",
enabled: false,
})
}
return options
}
// Allow all core roles
let options = roles.map(role => ({
name: enrichLabel(role.name),
_id: role._id,
}))
// Add creator if required
if (allowCreator) {
options.unshift({
_id: Constants.Roles.CREATOR,
name: "Can edit",
tag:
!$licensing.perAppBuildersEnabled &&
capitalise(Constants.PlanType.BUSINESS),
})
}
// Add remove option if required
if (allowRemove) {
newRoles = [
...newRoles,
{
_id: RemoveID,
name: "Remove",
},
]
options.push({
_id: RemoveID,
name: "Remove",
})
}
if (allowPublic) {
return newRoles
// Remove public if not allowed
if (!allowPublic) {
options = options.filter(role => role._id !== Constants.Roles.PUBLIC)
}
return newRoles.filter(role => role._id !== Constants.Roles.PUBLIC)
return options
}
const getColor = role => {
if (allowRemove && role._id === RemoveID) {
// Creator and remove options have no colors
if (role._id === Constants.Roles.CREATOR || role._id === RemoveID) {
return null
}
return RoleUtils.getRoleColour(role._id)
}
const getIcon = role => {
if (allowRemove && role._id === RemoveID) {
// Only remove option has an icon
if (role._id === RemoveID) {
return "Close"
}
return null

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,29 +2,19 @@
import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui"
import { store, currentAsset } from "builderStore"
import { tables, viewsV2 } from "stores/backend"
import {
getContextProviderComponents,
getSchemaForDatasourcePlus,
} from "builderStore/dataBinding"
import { getSchemaForDatasourcePlus } from "builderStore/dataBinding"
import SaveFields from "./SaveFields.svelte"
import { getDatasourceLikeProviders } from "components/design/settings/controls/ButtonActionEditor/actions/utils"
export let parameters
export let bindings = []
export let nested
$: formComponents = getContextProviderComponents(
$currentAsset,
$store.selectedComponentId,
"form",
{ includeSelf: nested }
)
$: schemaComponents = getContextProviderComponents(
$currentAsset,
$store.selectedComponentId,
"schema",
{ includeSelf: nested }
)
$: providerOptions = getProviderOptions(formComponents, schemaComponents)
$: providerOptions = getDatasourceLikeProviders({
asset: $currentAsset,
componentId: $store.selectedComponentId,
nested,
})
$: schemaFields = getSchemaFields(parameters?.tableId)
$: tableOptions = $tables.list.map(table => ({
label: table.name,
@ -36,40 +26,6 @@
}))
$: options = [...(tableOptions || []), ...(viewOptions || [])]
// Gets a context definition of a certain type from a component definition
const extractComponentContext = (component, contextType) => {
const def = store.actions.components.getDefinition(component?._component)
if (!def) {
return null
}
const contexts = Array.isArray(def.context) ? def.context : [def.context]
return contexts.find(context => context?.type === contextType)
}
// Gets options for valid context keys which provide valid data to submit
const getProviderOptions = (formComponents, schemaComponents) => {
const formContexts = formComponents.map(component => ({
component,
context: extractComponentContext(component, "form"),
}))
const schemaContexts = schemaComponents.map(component => ({
component,
context: extractComponentContext(component, "schema"),
}))
const allContexts = formContexts.concat(schemaContexts)
return allContexts.map(({ component, context }) => {
let runtimeBinding = component._id
if (context.suffix) {
runtimeBinding += `-${context.suffix}`
}
return {
label: component._instanceName,
value: runtimeBinding,
}
})
}
const getSchemaFields = resourceId => {
const { schema } = getSchemaForDatasourcePlus(resourceId)
return Object.values(schema || {})

View file

@ -1,22 +1,36 @@
<script>
import { currentAsset, store } from "builderStore"
import { Label, Combobox, Select } from "@budibase/bbui"
import {
getActionProviderComponents,
buildFormSchema,
} from "builderStore/dataBinding"
import { getActionProviders, buildFormSchema } from "builderStore/dataBinding"
import { findComponent } from "builderStore/componentUtils"
export let parameters
export let nested
$: formComponent = findComponent($currentAsset.props, parameters.componentId)
$: formComponent = getFormComponent(
$currentAsset.props,
parameters.componentId
)
$: formSchema = buildFormSchema(formComponent)
$: fieldOptions = Object.keys(formSchema || {})
$: actionProviders = getActionProviderComponents(
$: actionProviders = getActionProviders(
$currentAsset,
$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>
<div class="root">
@ -24,8 +38,8 @@
<Select
bind:value={parameters.componentId}
options={actionProviders}
getOptionLabel={x => x._instanceName}
getOptionValue={x => x._id}
getOptionLabel={x => x.readableBinding}
getOptionValue={x => x.runtimeBinding}
/>
<Label small>Field</Label>
<Combobox bind:value={parameters.field} options={fieldOptions} />

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