diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index b4f7739293..457d2c1451 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -31,6 +31,9 @@ A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. + +**App Export** +If possible - please attach an export of your budibase application for debugging/reproduction purposes. **Desktop (please complete the following information):** - OS: [e.g. iOS] diff --git a/.github/workflows/deploy-single-image.yml b/.github/workflows/deploy-single-image.yml index 0bd5c71a40..8bf8f232c5 100644 --- a/.github/workflows/deploy-single-image.yml +++ b/.github/workflows/deploy-single-image.yml @@ -57,3 +57,12 @@ jobs: platforms: linux/amd64,linux/arm64 tags: budibase/budibase,budibase/budibase:v${{ env.RELEASE_VERSION }} file: ./hosting/single/Dockerfile + - name: Tag and release Budibase Azure App Service docker image + uses: docker/build-push-action@v2 + with: + context: . + push: true + platforms: linux/amd64 + build-args: TARGETBUILD=aas + tags: budibase/budibase-aas,budibase/budibase-aas:v${{ env.RELEASE_VERSION }} + file: ./hosting/single/Dockerfile diff --git a/README.md b/README.md index e8c6475d90..ae149f7347 100644 --- a/README.md +++ b/README.md @@ -135,13 +135,18 @@ You can learn more about the Budibase API at the following places: ## 🏁 Get started - - Deploy Budibase self-hosted in your existing infrastructure, using Docker, Kubernetes, and Digital Ocean. Or use Budibase Cloud if you don't need to self-host, and would like to get started quickly. ### [Get started with self-hosting Budibase](https://docs.budibase.com/docs/hosting-methods) +- [Docker - single ARM compatible image](https://docs.budibase.com/docs/docker) +- [Docker Compose](https://docs.budibase.com/docs/docker-compose) +- [Kubernetes](https://docs.budibase.com/docs/kubernetes-k8s) +- [Digital Ocean](https://docs.budibase.com/docs/digitalocean) +- [Portainer](https://docs.budibase.com/docs/portainer) + + ### [Get started with Budibase Cloud](https://budibase.com) diff --git a/charts/budibase/templates/app-service-deployment.yaml b/charts/budibase/templates/app-service-deployment.yaml index 7a2c483cc8..fd46e77647 100644 --- a/charts/budibase/templates/app-service-deployment.yaml +++ b/charts/budibase/templates/app-service-deployment.yaml @@ -151,6 +151,10 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} + {{ if .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml .Values.imagePullSecrets | nindent 6 }} + {{ end }} restartPolicy: Always serviceAccountName: "" status: {} diff --git a/charts/budibase/templates/minio-service-deployment.yaml b/charts/budibase/templates/minio-service-deployment.yaml index 901fb61ad9..103f9e3ed2 100644 --- a/charts/budibase/templates/minio-service-deployment.yaml +++ b/charts/budibase/templates/minio-service-deployment.yaml @@ -68,6 +68,10 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} + {{ if .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml .Values.imagePullSecrets | nindent 6 }} + {{ end }} restartPolicy: Always serviceAccountName: "" volumes: @@ -75,4 +79,4 @@ spec: persistentVolumeClaim: claimName: minio-data status: {} -{{- end }} \ No newline at end of file +{{- end }} diff --git a/charts/budibase/templates/proxy-service-deployment.yaml b/charts/budibase/templates/proxy-service-deployment.yaml index bd6a5e311f..505a46f1e8 100644 --- a/charts/budibase/templates/proxy-service-deployment.yaml +++ b/charts/budibase/templates/proxy-service-deployment.yaml @@ -40,6 +40,10 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} + {{ if .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml .Values.imagePullSecrets | nindent 6 }} + {{ end }} restartPolicy: Always serviceAccountName: "" volumes: diff --git a/charts/budibase/templates/redis-service-deployment.yaml b/charts/budibase/templates/redis-service-deployment.yaml index 0b6cb12562..6e09346cad 100644 --- a/charts/budibase/templates/redis-service-deployment.yaml +++ b/charts/budibase/templates/redis-service-deployment.yaml @@ -47,6 +47,10 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} + {{ if .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml .Values.imagePullSecrets | nindent 6 }} + {{ end }} restartPolicy: Always serviceAccountName: "" volumes: @@ -54,4 +58,4 @@ spec: persistentVolumeClaim: claimName: redis-data status: {} -{{- end }} \ No newline at end of file +{{- end }} diff --git a/charts/budibase/templates/worker-service-deployment.yaml b/charts/budibase/templates/worker-service-deployment.yaml index a7f05f3137..918dab427b 100644 --- a/charts/budibase/templates/worker-service-deployment.yaml +++ b/charts/budibase/templates/worker-service-deployment.yaml @@ -145,6 +145,10 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} + {{ if .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml .Values.imagePullSecrets | nindent 6 }} + {{ end }} restartPolicy: Always serviceAccountName: "" status: {} diff --git a/hosting/docker-compose.dev.yaml b/hosting/docker-compose.dev.yaml index be0bc74a26..7322b0e8a9 100644 --- a/hosting/docker-compose.dev.yaml +++ b/hosting/docker-compose.dev.yaml @@ -11,10 +11,11 @@ services: - minio_data:/data ports: - "${MINIO_PORT}:9000" + - "9001:9001" environment: MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY} MINIO_SECRET_KEY: ${MINIO_SECRET_KEY} - command: server /data + command: server /data --console-address ":9001" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] interval: 30s diff --git a/hosting/docker-compose.yaml b/hosting/docker-compose.yaml index 57cbf33709..f669f9261d 100644 --- a/hosting/docker-compose.yaml +++ b/hosting/docker-compose.yaml @@ -63,7 +63,7 @@ services: MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY} MINIO_SECRET_KEY: ${MINIO_SECRET_KEY} MINIO_BROWSER: "off" - command: server /data + command: server /data --console-address ":9001" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] interval: 30s diff --git a/hosting/scripts/build-target-paths.sh b/hosting/scripts/build-target-paths.sh index d1c9b5cd05..4c165d12e7 100644 --- a/hosting/scripts/build-target-paths.sh +++ b/hosting/scripts/build-target-paths.sh @@ -3,15 +3,15 @@ echo ${TARGETBUILD} > /buildtarget.txt if [[ "${TARGETBUILD}" = "aas" ]]; then # Azure AppService uses /home for persisent data & SSH on port 2222 - mkdir -p /home/budibase/{minio,couchdb} - mkdir -p /home/budibase/couchdb/data - chown -R couchdb:couchdb /home/budibase/couchdb/ + mkdir -p /home/{search,minio,couch} + mkdir -p /home/couch/{dbs,views} + chown -R couchdb:couchdb /home/couch/ apt update apt-get install -y openssh-server - sed -i 's#dir=/opt/couchdb/data/search#dir=/home/budibase/couchdb/data/search#' /opt/clouseau/clouseau.ini - sed -i 's#/minio/minio server /minio &#/minio/minio server /home/budibase/minio &#' /runner.sh - sed -i 's#database_dir = ./data#database_dir = /home/budibase/couchdb/data#' /opt/couchdb/etc/default.ini - sed -i 's#view_index_dir = ./data#view_index_dir = /home/budibase/couchdb/data#' /opt/couchdb/etc/default.ini + sed -i 's#dir=/opt/couchdb/data/search#dir=/home/search#' /opt/clouseau/clouseau.ini + sed -i 's#/minio/minio server /minio &#/minio/minio server /home/minio &#' /runner.sh + sed -i 's#database_dir = ./data#database_dir = /home/couch/dbs#' /opt/couchdb/etc/default.ini + sed -i 's#view_index_dir = ./data#view_index_dir = /home/couch/views#' /opt/couchdb/etc/default.ini sed -i "s/#Port 22/Port 2222/" /etc/ssh/sshd_config /etc/init.d/ssh restart fi diff --git a/hosting/single/Dockerfile b/hosting/single/Dockerfile index 772ae2a8ab..b5bf17adde 100644 --- a/hosting/single/Dockerfile +++ b/hosting/single/Dockerfile @@ -108,7 +108,7 @@ RUN chmod +x install.sh && ./install.sh WORKDIR / ADD hosting/single/runner.sh . RUN chmod +x ./runner.sh -ADD hosting/scripts/healthcheck.sh . +ADD hosting/single/healthcheck.sh . RUN chmod +x ./healthcheck.sh ADD hosting/scripts/build-target-paths.sh . @@ -122,8 +122,7 @@ RUN yarn cache clean -f EXPOSE 80 EXPOSE 443 -VOLUME /opt/couchdb/data -VOLUME /minio +VOLUME /data # setup letsencrypt certificate RUN apt-get install -y certbot python3-certbot-nginx diff --git a/hosting/scripts/healthcheck.sh b/hosting/single/healthcheck.sh similarity index 93% rename from hosting/scripts/healthcheck.sh rename to hosting/single/healthcheck.sh index 80f2ece0b6..b92cd153a3 100644 --- a/hosting/scripts/healthcheck.sh +++ b/hosting/single/healthcheck.sh @@ -1,6 +1,10 @@ #!/usr/bin/env bash healthy=true +if [ -f "/data/.env" ]; then + export $(cat /data/.env | xargs) +fi + if [[ $(curl -Lfk -s -w "%{http_code}\n" http://localhost/ -o /dev/null) -ne 200 ]]; then echo 'ERROR: Budibase is not running'; healthy=false diff --git a/hosting/single/runner.sh b/hosting/single/runner.sh index f8c1fc5e56..9abb2fd093 100644 --- a/hosting/single/runner.sh +++ b/hosting/single/runner.sh @@ -24,8 +24,8 @@ if [ ! -f "/data/.env" ]; then fi # make these directories in runner, incase of mount -mkdir -p /data/couch/dbs /data/couch/views -chown couchdb:couchdb /data/couch /data/couch/dbs /data/couch/views +mkdir -p /data/couch/{dbs,views} /home/couch/{dbs,views} +chown -R couchdb:couchdb /data/couch /home/couch redis-server --requirepass $REDIS_PASSWORD & /opt/clouseau/bin/clouseau & /minio/minio server /data/minio & diff --git a/lerna.json b/lerna.json index ee80fb1271..ac87332bf0 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.1.10-alpha.3", + "version": "1.1.32-alpha.1", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/package.json b/package.json index 0c7d3989a2..4c24e0025b 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "build": "lerna run build", "build:dev": "lerna run prebuild && tsc --build --watch --preserveWatchOutput", "release": "lerna publish ${RELEASE_VERSION_TYPE:-patch} --yes --force-publish && yarn release:pro", - "release:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop && yarn release:pro:develop", + "release:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop --exact && yarn release:pro:develop", "release:pro": "bash scripts/pro/release.sh", "release:pro:develop": "bash scripts/pro/release.sh develop", "restore": "yarn run clean && yarn run bootstrap && yarn run build", @@ -85,4 +85,4 @@ "install:pro": "bash scripts/pro/install.sh", "dep:clean": "yarn clean && yarn bootstrap" } -} +} \ No newline at end of file diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 8156a4d0c0..4c10b3f709 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "1.1.10-alpha.3", + "version": "1.1.32-alpha.1", "description": "Budibase backend core libraries used in server and worker", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", @@ -20,7 +20,7 @@ "test:watch": "jest --watchAll" }, "dependencies": { - "@budibase/types": "^1.1.10-alpha.3", + "@budibase/types": "1.1.32-alpha.1", "@techpass/passport-openidconnect": "0.3.2", "aws-sdk": "2.1030.0", "bcrypt": "5.0.1", diff --git a/packages/backend-core/src/auth.js b/packages/backend-core/src/auth.js index b60144a0de..9ae29a3cbd 100644 --- a/packages/backend-core/src/auth.js +++ b/packages/backend-core/src/auth.js @@ -18,6 +18,8 @@ const { ssoCallbackUrl, csrf, internalApi, + adminOnly, + joiValidator, } = require("./middleware") const { invalidateUser } = require("./cache/user") @@ -173,4 +175,6 @@ module.exports = { refreshOAuthToken, updateUserOAuth, ssoCallbackUrl, + adminOnly, + joiValidator, } diff --git a/packages/backend-core/src/cache/writethrough.ts b/packages/backend-core/src/cache/writethrough.ts index e11ca0acaa..ec6b1604c8 100644 --- a/packages/backend-core/src/cache/writethrough.ts +++ b/packages/backend-core/src/cache/writethrough.ts @@ -1,5 +1,6 @@ import BaseCache from "./base" import { getWritethroughClient } from "../redis/init" +import { logWarn } from "../logging" const DEFAULT_WRITE_RATE_MS = 10000 let CACHE: BaseCache | null = null @@ -51,10 +52,8 @@ export async function put( if (err.status !== 409) { throw err } else { - // get the rev, update over it - this is risky, may change in future - const readDoc = await db.get(doc._id) - doc._rev = readDoc._rev - await writeDb(doc) + // Swallow 409s but log them + logWarn(`Ignoring conflict in write-through cache`) } } } diff --git a/packages/backend-core/src/context/index.ts b/packages/backend-core/src/context/index.ts index e0db18dde6..1e430f01de 100644 --- a/packages/backend-core/src/context/index.ts +++ b/packages/backend-core/src/context/index.ts @@ -67,6 +67,10 @@ export const getTenantIDFromAppID = (appId: string) => { // used for automations, API endpoints should always be in context already export const doInTenant = (tenantId: string | null, task: any) => { + // make sure default always selected in single tenancy + if (!env.MULTI_TENANCY) { + tenantId = tenantId || DEFAULT_TENANT_ID + } // the internal function is so that we can re-use an existing // context - don't want to close DB on a parent context async function internal(opts = { existing: false }) { diff --git a/packages/backend-core/src/db/constants.ts b/packages/backend-core/src/db/constants.ts index 716762dd45..9c6be25424 100644 --- a/packages/backend-core/src/db/constants.ts +++ b/packages/backend-core/src/db/constants.ts @@ -11,6 +11,7 @@ export enum AutomationViewModes { } export enum ViewNames { + USER_BY_APP = "by_app", USER_BY_EMAIL = "by_email2", BY_API_KEY = "by_api_key", USER_BY_BUILDERS = "by_builders", @@ -28,6 +29,7 @@ export const DeprecatedViews = { export enum DocumentTypes { USER = "us", + GROUP = "gr", WORKSPACE = "workspace", CONFIG = "config", TEMPLATE = "template", diff --git a/packages/backend-core/src/db/conversions.js b/packages/backend-core/src/db/conversions.js index 455cc712d8..90c04e9251 100644 --- a/packages/backend-core/src/db/conversions.js +++ b/packages/backend-core/src/db/conversions.js @@ -50,3 +50,8 @@ exports.getProdAppID = appId => { const rest = split.join(APP_DEV_PREFIX) return `${APP_PREFIX}${rest}` } + +exports.extractAppUUID = id => { + const split = id?.split("_") || [] + return split.length ? split[split.length - 1] : null +} diff --git a/packages/backend-core/src/db/pouch.js b/packages/backend-core/src/db/pouch.js index 59b7ff8ae7..12d7d787e3 100644 --- a/packages/backend-core/src/db/pouch.js +++ b/packages/backend-core/src/db/pouch.js @@ -102,6 +102,13 @@ exports.getPouch = (opts = {}) => { } } + if (opts.onDisk) { + POUCH_DB_DEFAULTS = { + prefix: undefined, + adapter: "leveldb", + } + } + if (opts.replication) { const replicationStream = require("pouchdb-replication-stream") PouchDB.plugin(replicationStream.plugin) diff --git a/packages/backend-core/src/db/utils.ts b/packages/backend-core/src/db/utils.ts index ba3f1dd3e9..8ab6fa6e98 100644 --- a/packages/backend-core/src/db/utils.ts +++ b/packages/backend-core/src/db/utils.ts @@ -8,7 +8,7 @@ import { doWithDB, allDbs } from "./index" import { getCouchInfo } from "./pouch" import { getAppMetadata } from "../cache/appMetadata" import { checkSlashesInUrl } from "../helpers" -import { isDevApp, isDevAppID } from "./conversions" +import { isDevApp, isDevAppID, getProdAppID } from "./conversions" import { APP_PREFIX } from "./constants" import * as events from "../events" @@ -107,6 +107,15 @@ export function getGlobalUserParams(globalId: any, otherProps: any = {}) { } } +export function getUsersByAppParams(appId: any, otherProps: any = {}) { + const prodAppId = getProdAppID(appId) + return { + ...otherProps, + startkey: prodAppId, + endkey: `${prodAppId}${UNICODE_MAX}`, + } +} + /** * Generates a template ID. * @param ownerId The owner/user of the template, this could be global or a workspace level. @@ -115,6 +124,10 @@ export function generateTemplateID(ownerId: any) { return `${DocumentTypes.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}${newid()}` } +export function generateAppUserID(prodAppId: string, userId: string) { + return `${prodAppId}${SEPARATOR}${userId}` +} + /** * Gets parameters for retrieving templates. Owner ID must be specified, either global or a workspace level. */ @@ -442,15 +455,29 @@ export const getPlatformUrl = async (opts = { tenantAware: true }) => { export function pagination( data: any[], pageSize: number, - { paginate, property } = { paginate: true, property: "_id" } + { + paginate, + property, + getKey, + }: { + paginate: boolean + property: string + getKey?: (doc: any) => string | undefined + } = { + paginate: true, + property: "_id", + } ) { if (!paginate) { return { data, hasNextPage: false } } const hasNextPage = data.length > pageSize let nextPage = undefined + if (!getKey) { + getKey = (doc: any) => (property ? doc?.[property] : doc?._id) + } if (hasNextPage) { - nextPage = property ? data[pageSize]?.[property] : data[pageSize]?._id + nextPage = getKey(data[pageSize]) } return { data: data.slice(0, pageSize), diff --git a/packages/backend-core/src/db/views.js b/packages/backend-core/src/db/views.js index 1e8dd7ee77..baf1807ca5 100644 --- a/packages/backend-core/src/db/views.js +++ b/packages/backend-core/src/db/views.js @@ -56,6 +56,33 @@ exports.createNewUserEmailView = async () => { await db.put(designDoc) } +exports.createUserAppView = async () => { + const db = getGlobalDB() + let designDoc + try { + designDoc = await db.get("_design/database") + } catch (err) { + // no design doc, make one + designDoc = DesignDoc() + } + const view = { + // if using variables in a map function need to inject them before use + map: `function(doc) { + if (doc._id.startsWith("${DocumentTypes.USER}${SEPARATOR}") && doc.roles) { + for (let prodAppId of Object.keys(doc.roles)) { + let emitted = prodAppId + "${SEPARATOR}" + doc._id + emit(emitted, null) + } + } + }`, + } + designDoc.views = { + ...designDoc.views, + [ViewNames.USER_BY_APP]: view, + } + await db.put(designDoc) +} + exports.createApiKeyView = async () => { const db = getGlobalDB() let designDoc @@ -106,6 +133,7 @@ exports.queryGlobalView = async (viewName, params, db = null) => { [ViewNames.USER_BY_EMAIL]: exports.createNewUserEmailView, [ViewNames.BY_API_KEY]: exports.createApiKeyView, [ViewNames.USER_BY_BUILDERS]: exports.createUserBuildersView, + [ViewNames.USER_BY_APP]: exports.createUserAppView, } // can pass DB in if working with something specific if (!db) { diff --git a/packages/backend-core/src/errors/index.js b/packages/backend-core/src/errors/index.js index 58b4eea8c5..31ffd739a0 100644 --- a/packages/backend-core/src/errors/index.js +++ b/packages/backend-core/src/errors/index.js @@ -37,6 +37,7 @@ module.exports = { types, errors: { UsageLimitError: licensing.UsageLimitError, + FeatureDisabledError: licensing.FeatureDisabledError, HTTPError: http.HTTPError, }, getPublicError, diff --git a/packages/backend-core/src/errors/licensing.js b/packages/backend-core/src/errors/licensing.js index 0d8ce08146..85d207ac35 100644 --- a/packages/backend-core/src/errors/licensing.js +++ b/packages/backend-core/src/errors/licensing.js @@ -4,6 +4,7 @@ const type = "license_error" const codes = { USAGE_LIMIT_EXCEEDED: "usage_limit_exceeded", + FEATURE_DISABLED: "feature_disabled", } const context = { @@ -12,6 +13,11 @@ const context = { limitName: err.limitName, } }, + [codes.FEATURE_DISABLED]: err => { + return { + featureName: err.featureName, + } + }, } class UsageLimitError extends HTTPError { @@ -21,9 +27,17 @@ class UsageLimitError extends HTTPError { } } +class FeatureDisabledError extends HTTPError { + constructor(message, featureName) { + super(message, 400, codes.FEATURE_DISABLED, type) + this.featureName = featureName + } +} + module.exports = { type, codes, context, UsageLimitError, + FeatureDisabledError, } diff --git a/packages/backend-core/src/events/publishers/group.ts b/packages/backend-core/src/events/publishers/group.ts new file mode 100644 index 0000000000..d300873725 --- /dev/null +++ b/packages/backend-core/src/events/publishers/group.ts @@ -0,0 +1,64 @@ +import { publishEvent } from "../events" +import { + Event, + UserGroup, + GroupCreatedEvent, + GroupDeletedEvent, + GroupUpdatedEvent, + GroupUsersAddedEvent, + GroupUsersDeletedEvent, + GroupAddedOnboardingEvent, + UserGroupRoles, +} from "@budibase/types" + +export async function created(group: UserGroup, timestamp?: number) { + const properties: GroupCreatedEvent = { + groupId: group._id as string, + } + await publishEvent(Event.USER_GROUP_CREATED, properties, timestamp) +} + +export async function updated(group: UserGroup) { + const properties: GroupUpdatedEvent = { + groupId: group._id as string, + } + await publishEvent(Event.USER_GROUP_UPDATED, properties) +} + +export async function deleted(group: UserGroup) { + const properties: GroupDeletedEvent = { + groupId: group._id as string, + } + await publishEvent(Event.USER_GROUP_DELETED, properties) +} + +export async function usersAdded(count: number, group: UserGroup) { + const properties: GroupUsersAddedEvent = { + count, + groupId: group._id as string, + } + await publishEvent(Event.USER_GROUP_USERS_ADDED, properties) +} + +export async function usersDeleted(emails: string[], group: UserGroup) { + const properties: GroupUsersDeletedEvent = { + count: emails.length, + groupId: group._id as string, + } + await publishEvent(Event.USER_GROUP_USERS_REMOVED, properties) +} + +export async function createdOnboarding(groupId: string) { + const properties: GroupAddedOnboardingEvent = { + groupId: groupId, + onboarding: true, + } + await publishEvent(Event.USER_GROUP_ONBOARDING, properties) +} + +export async function permissionsEdited(roles: UserGroupRoles) { + const properties: UserGroupRoles = { + ...roles, + } + await publishEvent(Event.USER_GROUP_PERMISSIONS_EDITED, properties) +} diff --git a/packages/backend-core/src/events/publishers/index.ts b/packages/backend-core/src/events/publishers/index.ts index 65785d4d8b..57fd0bf8e2 100644 --- a/packages/backend-core/src/events/publishers/index.ts +++ b/packages/backend-core/src/events/publishers/index.ts @@ -17,3 +17,4 @@ export * as user from "./user" export * as view from "./view" export * as installation from "./installation" export * as backfill from "./backfill" +export * as group from "./group" diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts index ab89eed3b2..35777ae817 100644 --- a/packages/backend-core/src/index.ts +++ b/packages/backend-core/src/index.ts @@ -3,6 +3,7 @@ const errorClasses = errors.errors import * as events from "./events" import * as migrations from "./migrations" import * as users from "./users" +import * as roles from "./security/roles" import * as accounts from "./cloud/accounts" import * as installation from "./installation" import env from "./environment" @@ -51,6 +52,7 @@ const core = { installation, errors, logging, + roles, ...errorClasses, } diff --git a/packages/backend-core/src/logging.ts b/packages/backend-core/src/logging.ts index 68c3307b2f..3fc79a5fe7 100644 --- a/packages/backend-core/src/logging.ts +++ b/packages/backend-core/src/logging.ts @@ -15,6 +15,22 @@ export function logAlert(message: string, e?: any) { console.error(`bb-alert: ${message} ${errorJson}`) } +export function logAlertWithInfo( + message: string, + db: string, + id: string, + error: any +) { + message = `${message} - db: ${db} - doc: ${id} - error: ` + logAlert(message, error) +} + +export function logWarn(message: string) { + console.warn(`bb-warn: ${message}`) +} + export default { logAlert, + logAlertWithInfo, + logWarn, } diff --git a/packages/backend-core/src/middleware/adminOnly.js b/packages/backend-core/src/middleware/adminOnly.js new file mode 100644 index 0000000000..4bfdf83848 --- /dev/null +++ b/packages/backend-core/src/middleware/adminOnly.js @@ -0,0 +1,9 @@ +module.exports = async (ctx, next) => { + if ( + !ctx.internal && + (!ctx.user || !ctx.user.admin || !ctx.user.admin.global) + ) { + ctx.throw(403, "Admin user only endpoint.") + } + return next() +} diff --git a/packages/backend-core/src/middleware/authenticated.js b/packages/backend-core/src/middleware/authenticated.js index 4e6e0b7ba2..d86af773c3 100644 --- a/packages/backend-core/src/middleware/authenticated.js +++ b/packages/backend-core/src/middleware/authenticated.js @@ -127,7 +127,7 @@ module.exports = ( } if (!user && tenantId) { user = { tenantId } - } else { + } else if (user) { delete user.password } // be explicit diff --git a/packages/backend-core/src/middleware/index.js b/packages/backend-core/src/middleware/index.js index 1721d56a3c..9d94bf5763 100644 --- a/packages/backend-core/src/middleware/index.js +++ b/packages/backend-core/src/middleware/index.js @@ -9,7 +9,8 @@ const tenancy = require("./tenancy") const internalApi = require("./internalApi") const datasourceGoogle = require("./passport/datasource/google") const csrf = require("./csrf") - +const adminOnly = require("./adminOnly") +const joiValidator = require("./joi-validator") module.exports = { google, oidc, @@ -25,4 +26,6 @@ module.exports = { google: datasourceGoogle, }, csrf, + adminOnly, + joiValidator, } diff --git a/packages/backend-core/src/middleware/joi-validator.js b/packages/backend-core/src/middleware/joi-validator.js new file mode 100644 index 0000000000..1686b0e727 --- /dev/null +++ b/packages/backend-core/src/middleware/joi-validator.js @@ -0,0 +1,28 @@ +function validate(schema, property) { + // Return a Koa middleware function + return (ctx, next) => { + if (!schema) { + return next() + } + let params = null + if (ctx[property] != null) { + params = ctx[property] + } else if (ctx.request[property] != null) { + params = ctx.request[property] + } + const { error } = schema.validate(params) + if (error) { + ctx.throw(400, `Invalid ${property} - ${error.message}`) + return + } + return next() + } +} + +module.exports.body = schema => { + return validate(schema, "body") +} + +module.exports.params = schema => { + return validate(schema, "params") +} diff --git a/packages/backend-core/src/objectStore/index.ts b/packages/backend-core/src/objectStore/index.ts index a7e0b0c134..503ab9bca0 100644 --- a/packages/backend-core/src/objectStore/index.ts +++ b/packages/backend-core/src/objectStore/index.ts @@ -75,9 +75,11 @@ export const ObjectStore = (bucket: any) => { s3ForcePathStyle: true, signatureVersion: "v4", apiVersion: "2006-03-01", - params: { + } + if (bucket) { + config.params = { Bucket: sanitizeBucket(bucket), - }, + } } if (env.MINIO_URL) { config.endpoint = env.MINIO_URL @@ -292,6 +294,7 @@ export const uploadDirectory = async ( } } await Promise.all(uploads) + return files } exports.downloadTarballDirect = async (url: string, path: string) => { diff --git a/packages/backend-core/src/security/roles.js b/packages/backend-core/src/security/roles.js index 7c57cadcbf..44dc4f2d3e 100644 --- a/packages/backend-core/src/security/roles.js +++ b/packages/backend-core/src/security/roles.js @@ -76,7 +76,7 @@ function isBuiltin(role) { /** * Works through the inheritance ranks to see how far up the builtin stack this ID is. */ -function builtinRoleToNumber(id) { +exports.builtinRoleToNumber = id => { const builtins = exports.getBuiltinRoles() const MAX = Object.values(BUILTIN_IDS).length + 1 if (id === BUILTIN_IDS.ADMIN || id === BUILTIN_IDS.BUILDER) { @@ -104,7 +104,8 @@ exports.lowerBuiltinRoleID = (roleId1, roleId2) => { if (!roleId2) { return roleId1 } - return builtinRoleToNumber(roleId1) > builtinRoleToNumber(roleId2) + return exports.builtinRoleToNumber(roleId1) > + exports.builtinRoleToNumber(roleId2) ? roleId2 : roleId1 } diff --git a/packages/backend-core/src/users.js b/packages/backend-core/src/users.js index 0c1350a674..34d546a8bb 100644 --- a/packages/backend-core/src/users.js +++ b/packages/backend-core/src/users.js @@ -1,4 +1,9 @@ -const { ViewNames } = require("./db/utils") +const { + ViewNames, + getUsersByAppParams, + getProdAppID, + generateAppUserID, +} = require("./db/utils") const { queryGlobalView } = require("./db/views") const { UNICODE_MAX } = require("./db/constants") @@ -13,12 +18,32 @@ exports.getGlobalUserByEmail = async email => { throw "Must supply an email address to view" } - const response = await queryGlobalView(ViewNames.USER_BY_EMAIL, { + return await queryGlobalView(ViewNames.USER_BY_EMAIL, { key: email.toLowerCase(), include_docs: true, }) +} - return response +exports.searchGlobalUsersByApp = async (appId, opts) => { + if (typeof appId !== "string") { + throw new Error("Must provide a string based app ID") + } + const params = getUsersByAppParams(appId, { + include_docs: true, + }) + params.startkey = opts && opts.startkey ? opts.startkey : params.startkey + let response = await queryGlobalView(ViewNames.USER_BY_APP, params) + if (!response) { + response = [] + } + return Array.isArray(response) ? response : [response] +} + +exports.getGlobalUserByAppPage = (appId, user) => { + if (!user) { + return + } + return generateAppUserID(getProdAppID(appId), user._id) } /** diff --git a/packages/backend-core/tests/utilities/mocks/events.js b/packages/backend-core/tests/utilities/mocks/events.js index a4055cc5ea..415d59019d 100644 --- a/packages/backend-core/tests/utilities/mocks/events.js +++ b/packages/backend-core/tests/utilities/mocks/events.js @@ -89,6 +89,14 @@ jest.spyOn(events.user, "passwordUpdated") jest.spyOn(events.user, "passwordResetRequested") jest.spyOn(events.user, "passwordReset") +jest.spyOn(events.group, "created") +jest.spyOn(events.group, "updated") +jest.spyOn(events.group, "deleted") +jest.spyOn(events.group, "usersAdded") +jest.spyOn(events.group, "usersDeleted") +jest.spyOn(events.group, "createdOnboarding") +jest.spyOn(events.group, "permissionsEdited") + jest.spyOn(events.serve, "servedBuilder") jest.spyOn(events.serve, "servedApp") jest.spyOn(events.serve, "servedAppPreview") diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 4b986d28dc..245a102230 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "1.1.10-alpha.3", + "version": "1.1.32-alpha.1", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", @@ -38,7 +38,7 @@ ], "dependencies": { "@adobe/spectrum-css-workflow-icons": "^1.2.1", - "@budibase/string-templates": "^1.1.10-alpha.3", + "@budibase/string-templates": "1.1.32-alpha.1", "@spectrum-css/actionbutton": "^1.0.1", "@spectrum-css/actiongroup": "^1.0.1", "@spectrum-css/avatar": "^3.0.2", diff --git a/packages/bbui/src/ActionButton/ActionButton.svelte b/packages/bbui/src/ActionButton/ActionButton.svelte index 53ba6c7e51..cfc810807e 100644 --- a/packages/bbui/src/ActionButton/ActionButton.svelte +++ b/packages/bbui/src/ActionButton/ActionButton.svelte @@ -84,6 +84,7 @@ } :global([dir="ltr"] .spectrum-ActionButton .spectrum-Icon) { margin-left: 0; + transition: color ease-out 130ms; } .is-selected:not(.spectrum-ActionButton--emphasized) { background: var(--spectrum-global-color-gray-300); @@ -92,4 +93,10 @@ padding: 0; min-width: 0; } + .spectrum-ActionButton--quiet { + padding: 0 8px; + } + .is-selected:not(.emphasized) .spectrum-Icon { + color: var(--spectrum-global-color-gray-900); + } diff --git a/packages/bbui/src/Avatar/Avatar.svelte b/packages/bbui/src/Avatar/Avatar.svelte index f8acd9024c..136a4fe24b 100644 --- a/packages/bbui/src/Avatar/Avatar.svelte +++ b/packages/bbui/src/Avatar/Avatar.svelte @@ -4,7 +4,7 @@ ["XXS", "--spectrum-alias-avatar-size-50"], ["XS", "--spectrum-alias-avatar-size-75"], ["S", "--spectrum-alias-avatar-size-200"], - ["M", "--spectrum-alias-avatar-size-300"], + ["M", "--spectrum-alias-avatar-size-400"], ["L", "--spectrum-alias-avatar-size-500"], ["XL", "--spectrum-alias-avatar-size-600"], ["XXL", "--spectrum-alias-avatar-size-700"], @@ -13,6 +13,19 @@ export let url = "" export let disabled = false export let initials = "JD" + + const DefaultColor = "#3aab87" + + $: color = getColor(initials) + + const getColor = initials => { + if (!initials?.length) { + return DefaultColor + } + const code = initials[0].toLowerCase().charCodeAt(0) + const hue = ((code % 26) / 26) * 360 + return `hsl(${hue}, 50%, 50%)` + } {#if url} @@ -25,10 +38,11 @@ /> {:else}
{initials || ""}
@@ -40,7 +54,6 @@ display: grid; place-items: center; font-weight: 600; - background: #3aab87; border-radius: 50%; overflow: hidden; user-select: none; diff --git a/packages/bbui/src/Drawer/Drawer.svelte b/packages/bbui/src/Drawer/Drawer.svelte index af09b014d0..e1880d0ed4 100644 --- a/packages/bbui/src/Drawer/Drawer.svelte +++ b/packages/bbui/src/Drawer/Drawer.svelte @@ -82,6 +82,7 @@ } .fillWidth { + left: 260px !important; width: calc(100% - 260px) !important; } diff --git a/packages/bbui/src/Form/Core/InputDropdown.svelte b/packages/bbui/src/Form/Core/InputDropdown.svelte new file mode 100644 index 0000000000..723b8ba9b1 --- /dev/null +++ b/packages/bbui/src/Form/Core/InputDropdown.svelte @@ -0,0 +1,218 @@ + + +
+
+ +
+
+ + {#if open} +
(open = false)} + transition:fly|local={{ y: -20, duration: 200 }} + class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open" + > +
    + {#each options as option, idx} +
  • onPick(getOptionValue(option, idx))} + > + + {getOptionLabel(option, idx)} + + +
  • + {/each} +
+
+ {/if} +
+
+ + diff --git a/packages/bbui/src/Form/Core/Multiselect.svelte b/packages/bbui/src/Form/Core/Multiselect.svelte index 3eb1add267..9dd5a25a4f 100644 --- a/packages/bbui/src/Form/Core/Multiselect.svelte +++ b/packages/bbui/src/Form/Core/Multiselect.svelte @@ -13,6 +13,7 @@ export let readonly = false export let autocomplete = false export let sort = false + export let autoWidth = false const dispatch = createEventDispatcher() $: selectedLookupMap = getSelectedLookupMap(value) @@ -85,4 +86,5 @@ {getOptionValue} onSelectOption={toggleOption} {sort} + {autoWidth} /> diff --git a/packages/bbui/src/Form/Core/Picker.svelte b/packages/bbui/src/Form/Core/Picker.svelte index fc9f801be2..cdaf00aded 100644 --- a/packages/bbui/src/Form/Core/Picker.svelte +++ b/packages/bbui/src/Form/Core/Picker.svelte @@ -87,10 +87,15 @@ on:mousedown={onClick} > {#if fieldIcon} - + {/if} + {#if fieldColour} + + + + {/if} {/if} - {#if fieldColour} - - - - {/if} onSelectOption(getOptionValue(option, idx))} > {#if getOptionIcon(option, idx)} - + {/if} + {#if getOptionColour(option, idx)} + + + + {/if} {getOptionLabel(option, idx)} @@ -180,11 +185,6 @@ > - {#if getOptionColour(option, idx)} - - - - {/if} {/each} {/if} @@ -209,6 +209,9 @@ width: 100%; box-shadow: none; } + .spectrum-Picker-label.auto-width { + margin-right: var(--spacing-xs); + } .spectrum-Picker-label:not(.auto-width) { overflow: hidden; text-overflow: ellipsis; @@ -221,16 +224,16 @@ .spectrum-Picker-label.auto-width.is-placeholder { padding-right: 2px; } + .auto-width .spectrum-Menu-item { + padding-right: var(--spacing-xl); + } /* Icon and colour alignment */ .spectrum-Menu-checkmark { align-self: center; margin-top: 0; } - .option-colour { - padding-left: 8px; - } - .option-icon { + .option-extra { padding-right: 8px; } diff --git a/packages/bbui/src/Form/Core/PickerDropdown.svelte b/packages/bbui/src/Form/Core/PickerDropdown.svelte new file mode 100644 index 0000000000..863403ee0c --- /dev/null +++ b/packages/bbui/src/Form/Core/PickerDropdown.svelte @@ -0,0 +1,430 @@ + + +
+
+ {#if iconData} + + + + {/if} + (primaryOpen = true)} + on:blur + on:focus + on:input + on:keyup + on:blur={onBlur} + on:input={onInput} + on:keyup={updateValueOnEnter} + value={primaryLabel || ""} + placeholder={placeholder || ""} + {disabled} + {readonly} + class="spectrum-Textfield-input spectrum-InputGroup-input" + class:labelPadding={iconData} + /> + {#if primaryValue} + + {/if} +
+ {#if primaryOpen} +
(primaryOpen = false)} + transition:fly|local={{ y: -20, duration: 200 }} + class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open" + class:auto-width={autoWidth} + class:is-full-width={!secondaryOptions.length} + > +
    + {#if placeholderOption} +
  • onSelectOption(null)} + > + {placeholderOption} + +
  • + {/if} + {#each groupTitles as title} +
    + {title} +
    + {#if primaryOptions} + {#each primaryOptions[title].data as option, idx} +
  • + onPickPrimary({ + value: primaryOptions[title].getValue(option), + label: primaryOptions[title].getLabel(option), + })} + > + {#if primaryOptions[title].getIcon(option)} +
    +
    + +
    +
    + {:else if getPrimaryOptionColour(option, idx)} + + + + {/if} + + + {primaryOptions[title].getLabel(option)} + + + + {#if getPrimaryOptionIcon(option, idx) && getPrimaryOptionColour(option, idx)} + + + + {/if} + +
  • + {/each} + {/if} + {/each} +
+
+ {/if} + {#if secondaryOptions.length} +
+ + {#if secondaryOpen} +
(secondaryOpen = false)} + transition:fly|local={{ y: -20, duration: 200 }} + class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open" + style="width: 30%" + > +
    + {#each secondaryOptions as option, idx} +
  • + onPickSecondary(getSecondaryOptionValue(option, idx))} + > + {#if getSecondaryOptionColour(option, idx)} + + + + {/if} + + + {getSecondaryOptionLabel(option, idx)} + + +
  • + {/each} +
+
+ {/if} +
+ {/if} +
+ + diff --git a/packages/bbui/src/Form/Core/Select.svelte b/packages/bbui/src/Form/Core/Select.svelte index 81d7ec8e6c..f549f58d0c 100644 --- a/packages/bbui/src/Form/Core/Select.svelte +++ b/packages/bbui/src/Form/Core/Select.svelte @@ -17,7 +17,6 @@ export let autoWidth = false export let autocomplete = false export let sort = false - const dispatch = createEventDispatcher() let open = false $: fieldText = getFieldText(value, options, placeholder) diff --git a/packages/bbui/src/Form/InputDropdown.svelte b/packages/bbui/src/Form/InputDropdown.svelte new file mode 100644 index 0000000000..73516ea37c --- /dev/null +++ b/packages/bbui/src/Form/InputDropdown.svelte @@ -0,0 +1,55 @@ + + + + + diff --git a/packages/bbui/src/Form/Multiselect.svelte b/packages/bbui/src/Form/Multiselect.svelte index 957dcccddf..7bcf22aa06 100644 --- a/packages/bbui/src/Form/Multiselect.svelte +++ b/packages/bbui/src/Form/Multiselect.svelte @@ -14,7 +14,7 @@ export let getOptionLabel = option => option export let getOptionValue = option => option export let sort = false - + export let autoWidth = false const dispatch = createEventDispatcher() const onChange = e => { value = e.detail @@ -33,6 +33,7 @@ {sort} {getOptionLabel} {getOptionValue} + {autoWidth} on:change={onChange} on:click /> diff --git a/packages/bbui/src/Form/PickerDropdown.svelte b/packages/bbui/src/Form/PickerDropdown.svelte new file mode 100644 index 0000000000..4ffb8248d0 --- /dev/null +++ b/packages/bbui/src/Form/PickerDropdown.svelte @@ -0,0 +1,125 @@ + + + + + diff --git a/packages/bbui/src/IconPicker/IconPicker.svelte b/packages/bbui/src/IconPicker/IconPicker.svelte new file mode 100644 index 0000000000..0e71be2c33 --- /dev/null +++ b/packages/bbui/src/IconPicker/IconPicker.svelte @@ -0,0 +1,177 @@ + + +
+
(open = true)}> +
+ +
+
+ {#if open} +
(open = false)} + transition:fly={{ y: -20, duration: 200 }} + class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open" + class:spectrum-Popover--align-right={alignRight} + > + {#each iconList as icon} +
+
{icon.label}
+
+ {#each icon.icons as icon} +
{ + onChange(icon) + }} + > + +
+ {/each} +
+
+ {/each} +
+ {/if} +
+ + diff --git a/packages/bbui/src/List/Items/DetailSummary.svench b/packages/bbui/src/List/Items/DetailSummary.svench deleted file mode 100644 index 48fb8f7df8..0000000000 --- a/packages/bbui/src/List/Items/DetailSummary.svench +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - -
- - 1 - 2 - 3 - 4 - - - 1 - 2 - 3 - 4 - -
-
- - -
- - 1 - 2 - 3 - 4 - - - 1 - 2 - 3 - 4 - -
-
diff --git a/packages/bbui/src/List/List.svelte b/packages/bbui/src/List/List.svelte new file mode 100644 index 0000000000..243b04da50 --- /dev/null +++ b/packages/bbui/src/List/List.svelte @@ -0,0 +1,28 @@ + + +
+ {#if title} +
+ {title} +
+ {/if} +
+ +
+
+ + diff --git a/packages/bbui/src/List/ListItem.svelte b/packages/bbui/src/List/ListItem.svelte new file mode 100644 index 0000000000..76a83e7b08 --- /dev/null +++ b/packages/bbui/src/List/ListItem.svelte @@ -0,0 +1,92 @@ + + +
+
+ {#if icon} +
+ +
+ {/if} + {#if avatar} + + {/if} + {#if title} + {title} + {/if} + {#if subtitle} + + {/if} +
+
+ +
+
+ + diff --git a/packages/bbui/src/StatusLight/StatusLight.svelte b/packages/bbui/src/StatusLight/StatusLight.svelte index a0c72443a6..5b7257891f 100644 --- a/packages/bbui/src/StatusLight/StatusLight.svelte +++ b/packages/bbui/src/StatusLight/StatusLight.svelte @@ -18,11 +18,16 @@ export let disabled = false export let active = false export let color = null + export let square = false + export let hoverable = false
diff --git a/packages/bbui/src/Table/AttachmentRenderer.svelte b/packages/bbui/src/Table/AttachmentRenderer.svelte index 97ce1394cc..4dff22aef8 100644 --- a/packages/bbui/src/Table/AttachmentRenderer.svelte +++ b/packages/bbui/src/Table/AttachmentRenderer.svelte @@ -1,5 +1,4 @@
diff --git a/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte b/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte index 9543a9c552..eb148534f3 100644 --- a/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte +++ b/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte @@ -11,6 +11,7 @@ Body, Icon, } from "@budibase/bbui" + import { TriggerStepID } from "constants/backend/automations" let name let selectedTrigger @@ -35,7 +36,7 @@ ) automationStore.actions.addBlockToAutomation(newBlock) - if (triggerVal.stepId === "WEBHOOK") { + if (triggerVal.stepId === TriggerStepID.WEBHOOK) { webhookModal.show } diff --git a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte index c149b6a00e..90e7ab661c 100644 --- a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte +++ b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte @@ -1,6 +1,7 @@
- - + (touched = true)} /> + {#if touched && !value} + + {/if}
diff --git a/packages/builder/src/components/automation/SetupPanel/FieldSelector.svelte b/packages/builder/src/components/automation/SetupPanel/FieldSelector.svelte new file mode 100644 index 0000000000..f510d961fb --- /dev/null +++ b/packages/builder/src/components/automation/SetupPanel/FieldSelector.svelte @@ -0,0 +1,114 @@ + + +{#if schemaFields.length && isTestModal} +
+ {#each schemaFields as [field, schema]} + + {/each} +
+{/if} + + diff --git a/packages/builder/src/components/automation/SetupPanel/TableSelector.svelte b/packages/builder/src/components/automation/SetupPanel/TableSelector.svelte index ceb28a37ca..1645ded66b 100644 --- a/packages/builder/src/components/automation/SetupPanel/TableSelector.svelte +++ b/packages/builder/src/components/automation/SetupPanel/TableSelector.svelte @@ -2,10 +2,16 @@ import { tables } from "stores/backend" import { Select } from "@budibase/bbui" import { createEventDispatcher } from "svelte" + import { TableNames } from "constants" const dispatch = createEventDispatcher() export let value + export let isTrigger + + $: filteredTables = $tables.list.filter(table => { + return !isTrigger || table._id !== TableNames.USERS + }) const onChange = e => { value = e.detail @@ -16,7 +22,7 @@ role.name} + getOptionValue={role => role._id} + getOptionColour={role => RoleUtils.getRoleColour(role._id)} + {placeholder} + {error} +/> diff --git a/packages/builder/src/components/common/bindings/DrawerBindableInput.svelte b/packages/builder/src/components/common/bindings/DrawerBindableInput.svelte index b162408b26..b8d418c62b 100644 --- a/packages/builder/src/components/common/bindings/DrawerBindableInput.svelte +++ b/packages/builder/src/components/common/bindings/DrawerBindableInput.svelte @@ -18,6 +18,7 @@ export let fillWidth export let allowJS = true export let updateOnChange = true + export let drawerLeft const dispatch = createEventDispatcher() let bindingDrawer @@ -53,7 +54,7 @@
{/if}
- + Add the objects on the left to enrich your text. diff --git a/packages/builder/src/components/deploy/CreateWebhookDeploymentModal.svelte b/packages/builder/src/components/deploy/CreateWebhookDeploymentModal.svelte index 19cea6db65..ee6b163a3e 100644 --- a/packages/builder/src/components/deploy/CreateWebhookDeploymentModal.svelte +++ b/packages/builder/src/components/deploy/CreateWebhookDeploymentModal.svelte @@ -3,6 +3,7 @@ import { ModalContent } from "@budibase/bbui" import { onMount } from "svelte" import WebhookDisplay from "../automation/Shared/WebhookDisplay.svelte" + import { TriggerStepID } from "constants/backend/automations" let webhookUrls = [] @@ -11,7 +12,7 @@ onMount(() => { webhookUrls = automations.map(automation => { const trigger = automation.definition.trigger - if (trigger?.stepId === "WEBHOOK" && trigger.inputs) { + if (trigger?.stepId === TriggerStepID.WEBHOOK && trigger.inputs) { return { type: "Automation", name: automation.name, diff --git a/packages/builder/src/components/deploy/DeployNavigation.svelte b/packages/builder/src/components/deploy/DeployNavigation.svelte index d12b31beaf..676d7a5b7f 100644 --- a/packages/builder/src/components/deploy/DeployNavigation.svelte +++ b/packages/builder/src/components/deploy/DeployNavigation.svelte @@ -56,6 +56,10 @@ } } + const previewApp = () => { + window.open(`/${application}`) + } + const viewApp = () => { analytics.captureEvent(Events.APP_VIEW_PUBLISHED, { appId: selectedApp.appId, @@ -174,7 +178,10 @@ Are you sure you want to unpublish the app {selectedApp?.name}? - +
+ + +
diff --git a/packages/builder/src/components/deploy/VersionModal.svelte b/packages/builder/src/components/deploy/VersionModal.svelte index f0f58d7cb0..b0f4655f1b 100644 --- a/packages/builder/src/components/deploy/VersionModal.svelte +++ b/packages/builder/src/components/deploy/VersionModal.svelte @@ -1,11 +1,11 @@ -{#if !hideIcon} -
- -
+{#if !hideIcon && updateAvailable} + + Update available + {/if} {title || ""}
- {#if showExpandIcon} + {#if expandable}
{/if} + {#if showCloseButton} + + {/if}
diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/TriggerAutomation.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/TriggerAutomation.svelte index fa32c88d65..4f661096c5 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/TriggerAutomation.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/TriggerAutomation.svelte @@ -2,6 +2,7 @@ import { Select, Label, Input, Checkbox } from "@budibase/bbui" import { automationStore } from "builderStore" import SaveFields from "./SaveFields.svelte" + import { TriggerStepID } from "constants/backend/automations" export let parameters = {} export let bindings = [] @@ -16,7 +17,7 @@ : AUTOMATION_STATUS.NEW $: automations = $automationStore.automations - .filter(a => a.definition.trigger?.stepId === "APP") + .filter(a => a.definition.trigger?.stepId === TriggerStepID.APP) .map(automation => { const schema = Object.entries( automation.definition.trigger.inputs.fields || {} diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ValidateForm.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ValidateForm.svelte index e572dc6c1c..e7f3d91ec8 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ValidateForm.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ValidateForm.svelte @@ -1,5 +1,5 @@ + +
+ +
+
+ {filtered.length} {title}{filtered.length === 1 ? "" : "s"} +
+
+ Add all +
+
+ +
+ {#each filtered as item} +
{ + select(item._id) + }} + style="padding-bottom: var(--spacing-m)" + class="selection" + > +
+ {item[key]} +
+ + {#if selected.includes(item._id)} +
+ +
+ {/if} +
+ {/each} +
+
+ + diff --git a/packages/builder/src/components/start/CreateAppModal.svelte b/packages/builder/src/components/start/CreateAppModal.svelte index 2cf1ce7f6c..23f9f3f80c 100644 --- a/packages/builder/src/components/start/CreateAppModal.svelte +++ b/packages/builder/src/components/start/CreateAppModal.svelte @@ -111,7 +111,6 @@ await admin.init() // Create user - await API.updateOwnMetadata({ roleId: $values.roleId }) await auth.setInitInfo({}) // Create a default home screen if no template was selected diff --git a/packages/builder/src/constants/backend/automations.js b/packages/builder/src/constants/backend/automations.js new file mode 100644 index 0000000000..e0cd5b6405 --- /dev/null +++ b/packages/builder/src/constants/backend/automations.js @@ -0,0 +1,28 @@ +export const TriggerStepID = { + ROW_SAVED: "ROW_SAVED", + ROW_UPDATED: "ROW_UPDATED", + ROW_DELETED: "ROW_DELETED", + WEBHOOK: "WEBHOOK", + APP: "APP", + CRON: "CRON", +} + +export const ActionStepID = { + SEND_EMAIL_SMTP: "SEND_EMAIL_SMTP", + CREATE_ROW: "CREATE_ROW", + UPDATE_ROW: "UPDATE_ROW", + DELETE_ROW: "DELETE_ROW", + OUTGOING_WEBHOOK: "OUTGOING_WEBHOOK", + EXECUTE_SCRIPT: "EXECUTE_SCRIPT", + EXECUTE_QUERY: "EXECUTE_QUERY", + SERVER_LOG: "SERVER_LOG", + DELAY: "DELAY", + FILTER: "FILTER", + QUERY_ROWS: "QUERY_ROWS", + LOOP: "LOOP", + // these used to be lowercase step IDs, maintain for backwards compat + discord: "discord", + slack: "slack", + zapier: "zapier", + integromat: "integromat", +} diff --git a/packages/builder/src/helpers/data/utils.js b/packages/builder/src/helpers/data/utils.js index 23aeb314a0..647c2be33e 100644 --- a/packages/builder/src/helpers/data/utils.js +++ b/packages/builder/src/helpers/data/utils.js @@ -150,12 +150,31 @@ export function flipHeaderState(headersActivity) { return enabled } +export const parseToCsv = (headers, rows) => { + let csv = headers?.map(key => `"${key}"`)?.join(",") || "" + + for (let row of rows) { + csv = `${csv}\n${headers + .map(header => { + let val = row[header] + val = + typeof val === "object" && !(val instanceof Date) + ? `"${JSON.stringify(val).replace(/"/g, "'")}"` + : `"${val}"` + return val.trim() + }) + .join(",")}` + } + return csv +} + export default { breakQueryString, buildQueryString, fieldsToSchema, flipHeaderState, keyValueToQueryParameters, + parseToCsv, queryParametersToKeyValue, schemaToFields, } diff --git a/packages/builder/src/main.js b/packages/builder/src/main.js index bc5ec4f009..dc1e1cf1bf 100644 --- a/packages/builder/src/main.js +++ b/packages/builder/src/main.js @@ -5,6 +5,8 @@ import "@spectrum-css/vars/dist/spectrum-darkest.css" import "@spectrum-css/vars/dist/spectrum-dark.css" import "@spectrum-css/vars/dist/spectrum-light.css" import "@spectrum-css/vars/dist/spectrum-lightest.css" +import "@budibase/frontend-core/src/themes/nord.css" +import "@budibase/frontend-core/src/themes/midnight.css" import "@spectrum-css/page/dist/index-vars.css" import "./global.css" import { suppressWarnings } from "./helpers/warnings" diff --git a/packages/builder/src/pages/builder/app/[application]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/_layout.svelte index df84277142..28c5fe18c6 100644 --- a/packages/builder/src/pages/builder/app/[application]/_layout.svelte +++ b/packages/builder/src/pages/builder/app/[application]/_layout.svelte @@ -23,10 +23,6 @@ $layout.children.find(layout => $isActive(layout.path))?.title ?? "data" ) - const previewApp = () => { - window.open(`/${application}`) - } - async function getPackage() { try { store.actions.reset() @@ -108,14 +104,10 @@
- +
+ +
-
@@ -183,4 +175,8 @@ align-items: center; gap: var(--spacing-xl); } + + .version { + margin-right: var(--spacing-s); + } diff --git a/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/rest/[query]/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/rest/[query]/index.svelte index 6a798f0178..5ccc173318 100644 --- a/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/rest/[query]/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/rest/[query]/index.svelte @@ -440,6 +440,7 @@ ...dynamicRequestBindings, ...dataSourceStaticBindings, ]} + bindingDrawerLeft="260px" /> @@ -448,6 +449,7 @@ name="param" headings bindings={mergedBindings} + bindingDrawerLeft="260px" /> @@ -458,6 +460,7 @@ name="header" headings bindings={mergedBindings} + bindingDrawerLeft="260px" /> diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPanel.svelte index d86e4a3c8d..c4b80dcc3a 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPanel.svelte @@ -1,10 +1,9 @@
@@ -15,23 +14,17 @@ options={$sortedScreens} getOptionLabel={x => x.routing.route} getOptionValue={x => x._id} - getOptionIcon={x => (x.routing.homeScreen ? "Home" : "WebPage")} getOptionColour={x => RoleUtils.getRoleColour(x.routing.roleId)} - bind:value={$store.selectedScreenId} + value={$store.selectedScreenId} + on:change={e => store.actions.screens.select(e.detail)} + quiet + autoWidth />
{#if $store.clientFeatures.devicePreview} {/if} -
@@ -58,6 +51,7 @@ justify-content: space-between; align-items: flex-start; gap: var(--spacing-l); + margin: 0 2px; } .header-left, .header-right { @@ -68,7 +62,8 @@ gap: var(--spacing-l); } .header-left :global(.spectrum-Picker) { - width: 250px; + font-weight: 600; + color: var(--spectrum-global-color-gray-900); } .content { flex: 1 1 auto; diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte index e332f8e896..304d41ad19 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte @@ -3,6 +3,7 @@ import { onMount, onDestroy } from "svelte" import { store, + selectedComponent, selectedScreen, selectedLayout, currentAsset, @@ -14,6 +15,7 @@ Layout, Heading, Body, + Icon, notifications, } from "@budibase/bbui" import ErrorSVG from "@budibase/frontend-core/assets/error.svg?raw" @@ -85,6 +87,10 @@ previewDevice: $store.previewDevice, messagePassing: $store.clientFeatures.messagePassing, navigation: $store.navigation, + hiddenComponentIds: + $store.componentToPaste?._id && $store.componentToPaste?.isCut + ? [$store.componentToPaste?._id] + : [], isBudibaseEvent: true, } @@ -92,6 +98,11 @@ $: json = JSON.stringify(previewData) $: refreshContent(json) + // Determine if the add component menu is active + $: isAddingComponent = $isActive( + `./components/${$selectedComponent?._id}/new` + ) + // Update the iframe with the builder info to render the correct preview const refreshContent = message => { if (iframe) { @@ -138,7 +149,7 @@ $goto("./components") } } else if (type === "update-prop") { - await store.actions.components.updateProp(data.prop, data.value) + await store.actions.components.updateSetting(data.prop, data.value) } else if (type === "delete-component" && data.id) { confirmDeleteComponent(data.id) } else if (type === "duplicate-component" && data.id) { @@ -215,6 +226,16 @@ idToDelete = null } + const toggleAddComponent = () => { + if (isAddingComponent) { + $goto(`../${$selectedScreen._id}/components/${$selectedComponent?._id}`) + } else { + $goto( + `../${$selectedScreen._id}/components/${$selectedComponent?._id}/new` + ) + } + } + onMount(() => { window.addEventListener("message", receiveMessage) if (!$store.clientFeatures.messagePassing) { @@ -278,6 +299,13 @@ class:tablet={$store.previewDevice === "tablet"} class:mobile={$store.previewDevice === "mobile"} /> +
+ Component +
diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/DevicePreviewSelect.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/DevicePreviewSelect.svelte index 9f9447daee..870f801336 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/DevicePreviewSelect.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/DevicePreviewSelect.svelte @@ -3,18 +3,21 @@ import { store } from "builderStore" - + store.actions.preview.setDevice("desktop")} /> store.actions.preview.setDevice("tablet")} /> store.actions.preview.setDevice("mobile")} diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/iframeTemplate.js b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/iframeTemplate.js index def32dd45f..1c789d858e 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/iframeTemplate.js +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/iframeTemplate.js @@ -65,7 +65,8 @@ export default ` theme, customTheme, previewDevice, - navigation + navigation, + hiddenComponentIds } = parsed // Set some flags so the app knows we're in the builder @@ -79,6 +80,7 @@ export default ` window["##BUDIBASE_PREVIEW_CUSTOM_THEME##"] = customTheme window["##BUDIBASE_PREVIEW_DEVICE##"] = previewDevice window["##BUDIBASE_PREVIEW_NAVIGATION##"] = navigation + window["##BUDIBASE_HIDDEN_COMPONENT_IDS##"] = hiddenComponentIds // Initialise app try { diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentDropdownMenu.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentDropdownMenu.svelte index ae031e14bd..ed66c66c29 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentDropdownMenu.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentDropdownMenu.svelte @@ -1,8 +1,6 @@ - $goto("../new")} - showExpandIcon - borderRight -> + +
+ +