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 @@
+
+
+
+
+
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}
- {#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 @@
+
+
+
+
+
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 @@
+
+
+
+
+ {#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 @@
{/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 @@
+
+
+
+
+
+
+ {#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
/>
@@ -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
->
+
+
+
+
-
@@ -110,6 +118,13 @@
diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/new/_components/componentStructure.json b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/new/_components/componentStructure.json
similarity index 100%
rename from packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/new/_components/componentStructure.json
rename to packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/new/_components/componentStructure.json
diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/new/index.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/new/index.svelte
new file mode 100644
index 0000000000..965254cf0d
--- /dev/null
+++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/new/index.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/new/_components/NewComponentTargetPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/new/_components/NewComponentTargetPanel.svelte
deleted file mode 100644
index af44934526..0000000000
--- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/new/_components/NewComponentTargetPanel.svelte
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
- Components that you add will be placed {position}
- {title}
-
-
-
diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/new/index.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/new/index.svelte
deleted file mode 100644
index 8f2042671b..0000000000
--- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/new/index.svelte
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/layouts/[layoutId]/_components/LayoutListPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/layouts/[layoutId]/_components/LayoutListPanel.svelte
index fd38f08ceb..cc895317fd 100644
--- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/layouts/[layoutId]/_components/LayoutListPanel.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/layouts/[layoutId]/_components/LayoutListPanel.svelte
@@ -13,7 +13,7 @@
indentLevel={0}
selected={$store.selectedLayoutId === layout._id}
text={layout.name}
- on:click={() => ($store.selectedLayoutId = layout._id)}
+ on:click={() => store.actions.layouts.select(layout._id)}
>
diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/RoleIndicator.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/RoleIndicator.svelte
new file mode 100644
index 0000000000..eb25d86645
--- /dev/null
+++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/RoleIndicator.svelte
@@ -0,0 +1,58 @@
+
+
+
(showTooltip = true)}
+ on:mouseleave={() => (showTooltip = false)}
+ style="--color: {color};"
+>
+
+ {#if showTooltip}
+
+
+
+ {/if}
+
+
+
diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/ScreenDropdownMenu.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/ScreenDropdownMenu.svelte
index 6d27ccc60b..0c35fa391e 100644
--- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/ScreenDropdownMenu.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/ScreenDropdownMenu.svelte
@@ -23,7 +23,7 @@
const pasteComponent = mode => {
try {
- store.actions.components.paste(screen.props, mode)
+ store.actions.components.paste(screen.props, mode, screen)
} catch (error) {
notifications.error("Error saving component")
}
@@ -50,7 +50,6 @@
await store.actions.screens.save(duplicateScreen)
} catch (error) {
notifications.error("Error duplicating screen")
- console.log(error)
}
}
diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/ScreenListPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/ScreenListPanel.svelte
index 40d9ab273d..a6fd9089b1 100644
--- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/ScreenListPanel.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/ScreenListPanel.svelte
@@ -1,11 +1,12 @@
-
+
+
{#each filteredScreens as screen (screen._id)}
($store.selectedScreenId = screen._id)}
- color={RoleUtils.getRoleColour(screen.routing.roleId)}
+ on:click={() => store.actions.screens.select(screen._id)}
+ rightAlignIcon
>
+
{/each}
{#if !filteredScreens?.length}
diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/ScreenSettingsPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/ScreenSettingsPanel.svelte
index 0d861d1cd8..e6779d542c 100644
--- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/ScreenSettingsPanel.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/ScreenSettingsPanel.svelte
@@ -1,7 +1,7 @@
+
+
diff --git a/packages/builder/src/pages/builder/portal/apps/index.svelte b/packages/builder/src/pages/builder/portal/apps/index.svelte
index de5ad178cb..0d05e170e0 100644
--- a/packages/builder/src/pages/builder/portal/apps/index.svelte
+++ b/packages/builder/src/pages/builder/portal/apps/index.svelte
@@ -20,12 +20,14 @@
import { store, automationStore } from "builderStore"
import { API } from "api"
import { onMount } from "svelte"
- import { apps, auth, admin, templates } from "stores/portal"
+ import { apps, auth, admin, templates, groups } from "stores/portal"
import download from "downloadjs"
import { goto } from "@roxi/routify"
import AppRow from "components/start/AppRow.svelte"
import { AppStatus } from "constants"
import Logo from "assets/bb-space-man.svg"
+ import AccessFilter from "./_components/AcessFilter.svelte"
+ import { Constants } from "@budibase/frontend-core"
let sortBy = "name"
let template
@@ -39,6 +41,7 @@
let cloud = $admin.cloud
let creatingFromTemplate = false
let automationErrors
+ let accessFilterList = null
const resolveWelcomeMessage = (auth, apps) => {
const userWelcome = auth?.user?.firstName
@@ -56,14 +59,20 @@
: "Start from scratch"
$: enrichedApps = enrichApps($apps, $auth.user, sortBy)
- $: filteredApps = enrichedApps.filter(app =>
- app?.name?.toLowerCase().includes(searchTerm.toLowerCase())
+ $: filteredApps = enrichedApps.filter(
+ app =>
+ app?.name?.toLowerCase().includes(searchTerm.toLowerCase()) &&
+ (accessFilterList !== null ? accessFilterList.includes(app?.appId) : true)
)
$: lockedApps = filteredApps.filter(app => app?.lockedYou || app?.lockedOther)
$: unlocked = lockedApps?.length === 0
$: automationErrors = getAutomationErrors(enrichedApps)
+ $: hasGroupsLicense = $auth.user?.license.features.includes(
+ Constants.Features.USER_GROUPS
+ )
+
const enrichApps = (apps, user, sortBy) => {
const enrichedApps = apps.map(app => ({
...app,
@@ -202,6 +211,10 @@
$goto(`../../app/${app.devId}`)
}
+ const accessFilterAction = accessFilter => {
+ accessFilterList = accessFilter.detail
+ }
+
function createAppFromTemplateUrl(templateKey) {
// validate the template key just to make sure
const templateParts = templateKey.split("/")
@@ -347,6 +360,9 @@
{/if}
+ {#if hasGroupsLicense && $groups.length}
+
+ {/if}
{#if $auth.isAdmin}
-
+
{/if}
diff --git a/packages/builder/src/pages/builder/portal/manage/groups/[groupId].svelte b/packages/builder/src/pages/builder/portal/manage/groups/[groupId].svelte
new file mode 100644
index 0000000000..2bcfd85cb6
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/manage/groups/[groupId].svelte
@@ -0,0 +1,226 @@
+
+
+
+
+
$goto("../groups")} size="S" icon="ArrowLeft">
+ Back
+
+
+
+
+
+ {#if group?.users.length}
+ {#each group.users as user}
+ removeUser(user?._id)}
+ hoverable
+ size="L"
+ name="Close"
+ />
+ {/each}
+ {:else}
+
+ {/if}
+
+
+
Apps
+
+ Manage apps that this User group has been assigned to
+
+
+
+
+ {#if groupApps.length}
+ {#each groupApps as app}
+
+
+
+
+ {group.roles[app.appId]}
+
+
+
+ {/each}
+ {:else}
+
+ {/if}
+
+
+
+
diff --git a/packages/builder/src/pages/builder/portal/manage/groups/_components/CreateEditGroupModal.svelte b/packages/builder/src/pages/builder/portal/manage/groups/_components/CreateEditGroupModal.svelte
new file mode 100644
index 0000000000..22a59c2193
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/manage/groups/_components/CreateEditGroupModal.svelte
@@ -0,0 +1,58 @@
+
+
+ saveGroup(group)}
+ size="M"
+ title="Create User Group"
+ confirmText="Save"
+>
+
+
+
+
+
diff --git a/packages/builder/src/pages/builder/portal/manage/groups/_components/UserGroupsRow.svelte b/packages/builder/src/pages/builder/portal/manage/groups/_components/UserGroupsRow.svelte
new file mode 100644
index 0000000000..e00123614a
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/manage/groups/_components/UserGroupsRow.svelte
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+ {parseInt(group?.users?.length) || 0} user{parseInt(
+ group?.users?.length
+ ) === 1
+ ? ""
+ : "s"}
+
+
+
+
+
+
+ {parseInt(group?.apps?.length) || 0} app{parseInt(group?.apps?.length) === 1
+ ? ""
+ : "s"}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/builder/src/pages/builder/portal/manage/groups/_layout.svelte b/packages/builder/src/pages/builder/portal/manage/groups/_layout.svelte
new file mode 100644
index 0000000000..a13211a9bb
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/manage/groups/_layout.svelte
@@ -0,0 +1,3 @@
+
+
+
diff --git a/packages/builder/src/pages/builder/portal/manage/groups/index.svelte b/packages/builder/src/pages/builder/portal/manage/groups/index.svelte
new file mode 100644
index 0000000000..131906529d
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/manage/groups/index.svelte
@@ -0,0 +1,145 @@
+
+
+
+
+
+
User groups
+ {#if !hasGroupsLicense}
+
+
+
+ {/if}
+
+ Easily assign and manage your users access with User Groups
+
+
+
+ {#if !hasGroupsLicense}
+
+ {/if}
+
+
+ {#if hasGroupsLicense && $groups.length}
+
+ {#each $groups as group}
+
+
+
+ {/each}
+
+ {/if}
+
+
+
+
+
+
+
diff --git a/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte b/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte
index a8cb340465..28c5aa2593 100644
--- a/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte
+++ b/packages/builder/src/pages/builder/portal/manage/users/[userId].svelte
@@ -2,79 +2,102 @@
import { goto } from "@roxi/routify"
import {
ActionButton,
+ ActionMenu,
+ Avatar,
Button,
Layout,
Heading,
Body,
- Divider,
Label,
+ List,
+ ListItem,
+ Icon,
Input,
+ MenuItem,
+ Popover,
Select,
- Toggle,
Modal,
- Table,
- ModalContent,
notifications,
+ StatusLight,
} from "@budibase/bbui"
+ import { onMount } from "svelte"
+
import { fetchData } from "helpers"
- import { users, auth } from "stores/portal"
-
- import TagsRenderer from "./_components/RolesTagsTableRenderer.svelte"
-
- import UpdateRolesModal from "./_components/UpdateRolesModal.svelte"
+ import { users, auth, groups, apps } from "stores/portal"
+ import { Constants } from "@budibase/frontend-core"
import ForceResetPasswordModal from "./_components/ForceResetPasswordModal.svelte"
+ import { RoleUtils } from "@budibase/frontend-core"
+ import UserGroupPicker from "components/settings/UserGroupPicker.svelte"
+ import DeleteUserModal from "./_components/DeleteUserModal.svelte"
export let userId
- let deleteUserModal
- let editRolesModal
+
+ let deleteModal
let resetPasswordModal
+ let popoverAnchor
+ let searchTerm = ""
+ let popover
+ let selectedGroups = []
+ let allAppList = []
+ let user
+ $: fetchUser(userId)
+ $: hasGroupsLicense = $auth.user?.license.features.includes(
+ Constants.Features.USER_GROUPS
+ )
- const roleSchema = {
- name: { displayName: "App" },
- role: {},
- }
-
- const noRoleSchema = {
- name: { displayName: "App" },
- }
-
- $: defaultRoleId = $userFetch?.data?.builder?.global ? "ADMIN" : ""
- // Merge the Apps list and the roles response to get something that makes sense for the table
- $: allAppList = Object.keys($apps?.data).map(id => {
- const roleId = $userFetch?.data?.roles?.[id] || defaultRoleId
- const role = $apps?.data?.[id].roles.find(role => role._id === roleId)
- return {
- ...$apps?.data?.[id],
- _id: id,
- role: [role],
- }
+ $: allAppList = $apps
+ .filter(x => {
+ if ($userFetch.data?.roles) {
+ return Object.keys($userFetch.data.roles).find(y => {
+ return x.appId === apps.extractAppId(y)
+ })
+ }
+ })
+ .map(app => {
+ let roles = Object.fromEntries(
+ Object.entries($userFetch.data.roles).filter(([key]) => {
+ return apps.extractAppId(key) === app.appId
+ })
+ )
+ return {
+ name: app.name,
+ devId: app.devId,
+ icon: app.icon,
+ roles,
+ }
+ })
+ // Used for searching through groups in the add group popover
+ $: filteredGroups = $groups.filter(
+ group =>
+ selectedGroups &&
+ group?.name?.toLowerCase().includes(searchTerm.toLowerCase())
+ )
+ $: userGroups = $groups.filter(x => {
+ return x.users?.find(y => {
+ return y._id === userId
+ })
})
- $: appList = allAppList.filter(app => !!app.role[0])
- $: noRoleAppList = allAppList
- .filter(app => !app.role[0])
- .map(app => {
- delete app.role
- return app
- })
-
- let selectedApp
+ $: globalRole = $userFetch?.data?.admin?.global
+ ? "admin"
+ : $userFetch?.data?.builder?.global
+ ? "developer"
+ : "appUser"
const userFetch = fetchData(`/api/global/users/${userId}`)
- const apps = fetchData(`/api/global/roles`)
- async function deleteUser() {
- try {
- await users.delete(userId)
- notifications.success(`User ${$userFetch?.data?.email} deleted.`)
- $goto("./")
- } catch (error) {
- notifications.error("Error deleting user")
- }
+ function getHighestRole(roles) {
+ let highestRole
+ let highestRoleNumber = 0
+ Object.keys(roles).forEach(role => {
+ let roleNumber = RoleUtils.getRolePriority(roles[role])
+ if (roleNumber > highestRoleNumber) {
+ highestRoleNumber = roleNumber
+ highestRole = roles[role]
+ }
+ })
+ return highestRole
}
-
- let toggleDisabled = false
-
async function updateUserFirstName(evt) {
try {
await users.save({ ...$userFetch?.data, firstName: evt.target.value })
@@ -84,6 +107,13 @@
}
}
+ async function removeGroup(id) {
+ let updatedGroup = $groups.find(x => x._id === id)
+ let newUsers = updatedGroup.users.filter(user => user._id !== userId)
+ updatedGroup.users = newUsers
+ groups.actions.save(updatedGroup)
+ }
+
async function updateUserLastName(evt) {
try {
await users.save({ ...$userFetch?.data, lastName: evt.target.value })
@@ -93,61 +123,95 @@
}
}
- async function toggleFlag(flagName, detail) {
- toggleDisabled = true
+ async function updateUserRole({ detail }) {
+ if (detail === "developer") {
+ toggleFlags({ admin: { global: false }, builder: { global: true } })
+ } else if (detail === "admin") {
+ toggleFlags({ admin: { global: true }, builder: { global: false } })
+ } else if (detail === "appUser") {
+ toggleFlags({ admin: { global: false }, builder: { global: false } })
+ }
+ }
+
+ async function addGroup(groupId) {
+ let selectedGroup = selectedGroups.includes(groupId)
+ let group = $groups.find(group => group._id === groupId)
+
+ if (selectedGroup) {
+ selectedGroups = selectedGroups.filter(id => id === selectedGroup)
+ let newUsers = group.users.filter(groupUser => user._id !== groupUser._id)
+ group.users = newUsers
+ } else {
+ selectedGroups = [...selectedGroups, groupId]
+ group.users.push(user)
+ }
+
+ await groups.actions.save(group)
+ }
+
+ async function fetchUser(userId) {
+ let userPromise = users.get(userId)
+ user = await userPromise
+ }
+
+ async function toggleFlags(detail) {
try {
- await users.save({ ...$userFetch?.data, [flagName]: { global: detail } })
+ await users.save({ ...$userFetch?.data, ...detail })
await userFetch.refresh()
} catch (error) {
notifications.error("Error updating user")
}
- toggleDisabled = false
}
- async function toggleBuilderAccess({ detail }) {
- return toggleFlag("builder", detail)
- }
-
- async function toggleAdminAccess({ detail }) {
- return toggleFlag("admin", detail)
- }
-
- async function openUpdateRolesModal({ detail }) {
- selectedApp = detail
- editRolesModal.show()
- }
+ function addAll() {}
+ onMount(async () => {
+ try {
+ await groups.actions.init()
+ await apps.load()
+ } catch (error) {
+ notifications.error("Error getting User groups")
+ }
+ })
-
+
-
$goto("./")}
- quiet
- size="S"
- icon="BackAndroid"
- >
- Back to users
+ $goto("./")} size="S" icon="ArrowLeft">
+ Back
- User: {$userFetch?.data?.email}
-
- Change user settings and update their app roles. Also contains the ability
- to delete the user as well as force reset their password.
-
-
+
+
+
+
+
+
+ {$userFetch?.data?.firstName +
+ " " +
+ $userFetch?.data?.lastName}
+ {$userFetch?.data?.email}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- General
-
-
-
-
-
-
-
-
{#if userId !== $auth.user._id}
-
-
-
-
-
- Role
+
{/if}
-
-
Force password reset
+
+
+ {#if hasGroupsLicense}
+
+
+
+
+ User groups
+ Add or remove this user from user groups
+
+
+
+
+
+
+
+
+
+
+ {#if userGroups.length}
+ {#each userGroups as group}
+
+ {/each}
+ {:else}
+
+ {/if}
+
+
+ {/if}
+
+
+
+
Apps
+
+ Manage apps that this user has been assigned to
+
+
+
+ {#if allAppList.length}
+ {#each allAppList as app}
+
+
+
+
+
+ {Constants.Roles[getHighestRole(app.roles)]}
+
+
+
+
+ {/each}
+ {:else}
+
+ {/if}
+
-
-
- Configure roles
- Specify a role to grant access to an app.
-
-
-
- No Access
- Apps do not appear in the users portal. Public pages may still be viewed
- if visited directly.
-
-
-
-
- Delete user
- Deleting a user completely removes them from your account.
-
-
-
-
-
-
-
- Are you sure you want to delete {$userFetch?.data?.email}
-
-
-
-
-
+
+
diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/AddUserModal.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/AddUserModal.svelte
index 88a8fb6c5d..1997d4292a 100644
--- a/packages/builder/src/pages/builder/portal/manage/users/_components/AddUserModal.svelte
+++ b/packages/builder/src/pages/builder/portal/manage/users/_components/AddUserModal.svelte
@@ -1,113 +1,86 @@
+ showOnboardingTypeModal({ users: userData, groups: userGroups })}
size="M"
title="Add new user"
confirmText="Add user"
confirmDisabled={disabled}
cancelText="Cancel"
- disabled={$error}
showCloseIcon={false}
>
-
- If you have SMTP configured and an email for the new user, you can use the
- automated email onboarding flow. Otherwise, use our basic onboarding process
- with autogenerated passwords.
-
-
+
+
-
+ {#each userData as input, index}
+
+ {/each}
+
+
- {#if basic}
-
+ {#if hasGroupsLicense}
+ option.name}
+ getOptionValue={option => option._id}
+ />
{/if}
-
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/AppsTableRenderer.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/AppsTableRenderer.svelte
new file mode 100644
index 0000000000..d348082ffa
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/manage/users/_components/AppsTableRenderer.svelte
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ {parseInt(value?.length) || 0}
+
+
+
diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/DeleteUserModal.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/DeleteUserModal.svelte
new file mode 100644
index 0000000000..946fa430d2
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/manage/users/_components/DeleteUserModal.svelte
@@ -0,0 +1,31 @@
+
+
+
+
+ Are you sure you want to delete {user?.email}
+
+
diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/EmailSelect.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/EmailSelect.svelte
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/GroupsTableRenderer.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/GroupsTableRenderer.svelte
new file mode 100644
index 0000000000..772b5fe7b9
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/manage/users/_components/GroupsTableRenderer.svelte
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+ {#if value?.length === 0}
+
0
+ {:else if value?.length === 1}
+
+ {value[0]?.name}
+
+ {:else}
+
+ {parseInt(value?.length) || 0} groups
+
+ {/if}
+
+
+
diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/ImportUsersModal.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/ImportUsersModal.svelte
new file mode 100644
index 0000000000..64334b4ab2
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/manage/users/_components/ImportUsersModal.svelte
@@ -0,0 +1,157 @@
+
+
+ createUsersFromCsv({ userEmails, usersRole, userGroups })}
+ disabled={!userEmails.length || !validEmails(userEmails) || !usersRole}
+>
+ Import your users email addrresses from a CSV
+
+
+
+
+
+
+
+
+ {#if hasGroupsLicense}
+ option.name}
+ getOptionValue={option => option._id}
+ />
+ {/if}
+
+
+
diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/NameTableRenderer.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/NameTableRenderer.svelte
new file mode 100644
index 0000000000..af61ea2d57
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/manage/users/_components/NameTableRenderer.svelte
@@ -0,0 +1,38 @@
+
+
+
+ {#if value}
+
+
x[0])
+ .join("")}
+ />
+
+ {value}
+ {:else}
+
Not Available
+ {/if}
+
+
+
diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/OnboardingTypeModal.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/OnboardingTypeModal.svelte
new file mode 100644
index 0000000000..7ec6d338d5
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/manage/users/_components/OnboardingTypeModal.svelte
@@ -0,0 +1,108 @@
+
+
+ chooseCreationType(selectedOnboardingType)}
+ disabled={!selectedOnboardingType}
+>
+
+ {
+ selectedOnboardingType = emailOnboardingKey
+ }}
+ >
+
+
+
+ Send email invites
+
+
+
+ {#if selectedOnboardingType == emailOnboardingKey}
+
+
+
+ {/if}
+
+
+
+ {
+ selectedOnboardingType = basicOnboaridngKey
+ }}
+ >
+
+
+
+ Generate passwords for each user
+
+
+
+ {#if selectedOnboardingType == basicOnboaridngKey}
+
+
+
+ {/if}
+
+
+
+
+
+
diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/PasswordCopyRenderer.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/PasswordCopyRenderer.svelte
new file mode 100644
index 0000000000..00e0c6eeab
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/manage/users/_components/PasswordCopyRenderer.svelte
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/PasswordModal.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/PasswordModal.svelte
new file mode 100644
index 0000000000..e2995d8a02
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/manage/users/_components/PasswordModal.svelte
@@ -0,0 +1,94 @@
+
+
+
+ All your new users can be accessed through the autogenerated passwords.
+ Make not of these passwords or download the csv
+
+
+
+
+
+
+ Passwords CSV
+
+
+
+
+
+
+
+
diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/RoleTableRenderer.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/RoleTableRenderer.svelte
new file mode 100644
index 0000000000..4f481d374c
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/manage/users/_components/RoleTableRenderer.svelte
@@ -0,0 +1,16 @@
+
+
+
+ {value}
+
+
+
diff --git a/packages/builder/src/pages/builder/portal/manage/users/index.svelte b/packages/builder/src/pages/builder/portal/manage/users/index.svelte
index 5a5f6c987a..952acaf324 100644
--- a/packages/builder/src/pages/builder/portal/manage/users/index.svelte
+++ b/packages/builder/src/pages/builder/portal/manage/users/index.svelte
@@ -1,52 +1,232 @@
+
+
+
+ {#if appGroups.length || appUsers.length}
+
+
Access
+
+
+ Assign users to your app and define their access here
+
+
+
+ {#if hasGroupsLicense && appGroups.length}
+
+ {#each appGroups as group}
+
+ updateGroupRole(e.detail, group)}
+ autoWidth
+ quiet
+ value={group.roles[
+ Object.keys(group.roles).find(x => x === fixedAppId)
+ ]}
+ />
+ removeGroup(group)}
+ hoverable
+ size="S"
+ name="Close"
+ />
+
+ {/each}
+
+ {/if}
+ {#if appUsers.length}
+
+ {#each appUsers as user}
+
+ updateUserRole(e.detail, user)}
+ autoWidth
+ quiet
+ value={user.roles[
+ Object.keys(user.roles).find(x => x === fixedAppId)
+ ]}
+ />
+ removeUser(user)}
+ hoverable
+ size="S"
+ name="Close"
+ />
+
+ {/each}
+
+
+ {/if}
+ {:else}
+
+
+ No users assigned
+
+ Assign users to your app and set their access here
+
+
+
+
+
+
+ {/if}
+
+
+
+
+
+
+
+
diff --git a/packages/builder/src/pages/builder/portal/overview/_components/AssignmentModal.svelte b/packages/builder/src/pages/builder/portal/overview/_components/AssignmentModal.svelte
new file mode 100644
index 0000000000..aee7a8aa7d
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/overview/_components/AssignmentModal.svelte
@@ -0,0 +1,103 @@
+
+
+ addData(appData)}
+ showCloseIcon={false}
+>
+ {#each appData as input, index}
+ group.name}
+ getPrimaryOptionValue={group => group.name}
+ getPrimaryOptionIcon={group => group.icon}
+ getPrimaryOptionColour={group => group.colour}
+ getSecondaryOptionLabel={role => role.name}
+ getSecondaryOptionValue={role => role._id}
+ getSecondaryOptionColour={role => RoleUtils.getRoleColour(role._id)}
+ />
+ {/each}
+
+
+
diff --git a/packages/builder/src/pages/builder/portal/overview/_components/OverviewTab.svelte b/packages/builder/src/pages/builder/portal/overview/_components/OverviewTab.svelte
index a1b9530c30..6693c285ff 100644
--- a/packages/builder/src/pages/builder/portal/overview/_components/OverviewTab.svelte
+++ b/packages/builder/src/pages/builder/portal/overview/_components/OverviewTab.svelte
@@ -1,16 +1,17 @@
@@ -132,6 +137,37 @@
{/if}
+ {
+ navigateTab("Access")
+ }}
+ dataCy={"access"}
+ >
+
+ {#if $users?.data?.length}
+
+
+ {#each $users?.data as user}
+
+ {/each}
+
+
+
+ {$users?.data.length} users have access to this app
+
+
+ {:else}
+
+ No users
+
+ No users have been assigned to this app
+
+
+ {/if}
+
+
{#if false}
@@ -186,6 +222,14 @@
grid-template-columns: repeat(auto-fill, minmax(30%, 1fr));
}
+ .users-tab {
+ display: flex;
+ gap: var(--spacing-m);
+ }
+
+ .users-text {
+ color: var(--spectrum-global-color-gray-600);
+ }
.overview-tab .bottom,
.automation-metrics {
display: grid;
diff --git a/packages/builder/src/pages/builder/portal/overview/_components/SettingsTab.svelte b/packages/builder/src/pages/builder/portal/overview/_components/SettingsTab.svelte
index a00694624b..8efa5a81e4 100644
--- a/packages/builder/src/pages/builder/portal/overview/_components/SettingsTab.svelte
+++ b/packages/builder/src/pages/builder/portal/overview/_components/SettingsTab.svelte
@@ -66,21 +66,26 @@
The app is currently using version
{$store.version}
- but version
{clientPackage.version} is available.
+ but version
{clientPackage.version} is
+ available.
+
+ Updates can contain new features, performance improvements and bug
+ fixes.
+
+
+
{:else}
-
+
The app is currently using version
{$store.version}. You're running the latest!
-
+
+
+
+
{/if}
-
- Updates can contain new features, performance improvements and bug
- fixes.
-
-
-
-