diff --git a/.github/workflows/README.md b/.github/workflows/README.md
index c33665c964..f77323d85a 100644
--- a/.github/workflows/README.md
+++ b/.github/workflows/README.md
@@ -119,6 +119,8 @@ This job is responsible for deploying to our production, cloud kubernetes enviro
## Pro
+| **NOTE**: When developing for both pro / budibase repositories, your branch names need to match, or else the correct pro doesn't get run within your CI job.
+
### Installing Pro
The pro package is always installed from source in our CI jobs.
@@ -132,7 +134,7 @@ This is done to prevent pro needing to be published prior to CI runs in budiabse
- backend-core lives in the monorepo, so it can't be released independently to be used in pro
- therefore the only option is to pull pro from source and release it as a part of the monorepo release, as if it were a mono package
-The install is performed using the same steps as local development, via the `yarn bootstrap` command, see the [Contributing Guide#Pro](../CONTRIBUTING.md#pro)
+The install is performed using the same steps as local development, via the `yarn bootstrap` command, see the [Contributing Guide#Pro](../../docs/CONTRIBUTING.md#pro)
The branch to install pro from can vary depending on ref of the commit that triggered the budibase CI job. This is done to enable branches which have changes in both the monorepo and the pro repo to have their CI pass successfully.
diff --git a/.github/workflows/release-develop.yml b/.github/workflows/release-develop.yml
index 631308d945..772fcf933d 100644
--- a/.github/workflows/release-develop.yml
+++ b/.github/workflows/release-develop.yml
@@ -18,8 +18,9 @@ on:
workflow_dispatch:
env:
- # Posthog token used by ui at build time
- POSTHOG_TOKEN: phc_uDYOfnFt6wAbBAXkC6STjcrTpAFiWIhqgFcsC1UVO5F
+ # Posthog token used by ui at build time
+ # disable unless needed for testing
+ # POSTHOG_TOKEN: phc_uDYOfnFt6wAbBAXkC6STjcrTpAFiWIhqgFcsC1UVO5F
INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
FEATURE_PREVIEW_URL: https://budirelease.live
diff --git a/README.md b/README.md
index ae149f7347..1dec1737da 100644
--- a/README.md
+++ b/README.md
@@ -169,7 +169,7 @@ If you have a question or would like to talk with other Budibase users and join
## ❗ Code of conduct
-Budibase is dedicated to providing a welcoming, diverse, and harrassment-free experience for everyone. We expect everyone in the Budibase community to abide by our [**Code of Conduct**](https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md). Please read it.
+Budibase is dedicated to providing a welcoming, diverse, and harrassment-free experience for everyone. We expect everyone in the Budibase community to abide by our [**Code of Conduct**](https://github.com/Budibase/budibase/blob/HEAD/docs/CODE_OF_CONDUCT.md). Please read it.
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
index 531ed05749..fb0848596c 100644
--- a/docs/CONTRIBUTING.md
+++ b/docs/CONTRIBUTING.md
@@ -4,10 +4,10 @@ From opening a bug report to creating a pull request: every contribution is appr
## Table of contents
-- [Quick start](#quick-start)
-- [Status](#status)
-- [What's included](#whats-included)
-- [Bugs and feature requests](#bugs-and-feature-requests)
+- [Where to start](#not-sure-where-to-start)
+- [Contributor Licence Agreement](#contributor-license-agreement-cla)
+- [Glossary of Terms](#glossary-of-terms)
+- [Contributing to Budibase](#contributing-to-budibase)
## Not Sure Where to Start?
@@ -32,6 +32,9 @@ All contributors must sign an [Individual Contributor License Agreement](https:/
If contributing on behalf of your company, your company must sign a [Corporate Contributor License Agreement](https://github.com/budibase/budibase/blob/next/.github/cla/corporate-cla.md). If so, please contact us via community@budibase.com.
+If for any reason, your first contribution is in a PR created by other contributor, please just add a comment to the PR
+with the following text to agree our CLA: "I have read the CLA Document and I hereby sign the CLA".
+
## Glossary of Terms
To understand the budibase API, it can be helpful to understand the top level entities that make up Budibase.
@@ -162,7 +165,10 @@ When you are running locally, budibase stores data on disk using docker volumes.
### Development Modes
-A combination of environment variables controls the mode budibase runs in.
+A combination of environment variables controls the mode budibase runs in.
+
+| **NOTE**: You need to clean your browser cookies when you change between different modes.
+
Yarn commands can be used to mimic the different modes as described in the sections below:
#### Self Hosted
@@ -189,7 +195,7 @@ To enable this mode, use:
yarn mode:account
```
### CI
- An overview of the CI pipelines can be found [here](./workflows/README.md)
+ An overview of the CI pipelines can be found [here](../.github/workflows/README.md)
### Pro
diff --git a/docs/DEV-SETUP-MACOSX.md b/docs/DEV-SETUP-MACOSX.md
index 5606fd0d10..c5990e58da 100644
--- a/docs/DEV-SETUP-MACOSX.md
+++ b/docs/DEV-SETUP-MACOSX.md
@@ -4,6 +4,11 @@
Install instructions [here](https://brew.sh/)
+| **NOTE**: If you are working on a M1 Apple Silicon which is running Z shell, you could need to add
+`eval $(/opt/homebrew/bin/brew shellenv)` line to your `.zshrc`. This will make your zsh to find the apps you install
+through brew.
+
+
### Install Node
Budibase requires a recent version of node (14+):
@@ -51,4 +56,7 @@ So this command will actually run the application in dev mode. It creates .env f
The dev version will be available on port 10000 i.e.
-http://127.0.0.1:10000/builder/admin
\ No newline at end of file
+http://127.0.0.1:10000/builder/admin
+
+| **NOTE**: If you are working on a M1 Apple Silicon, you will need to uncomment `# platform: linux/amd64` line in
+[hosting/docker-compose-dev.yaml](../hosting/docker-compose.dev.yaml)
\ No newline at end of file
diff --git a/i18n/README.es.md b/i18n/README.es.md
index 7245dc8656..21eb8caef7 100644
--- a/i18n/README.es.md
+++ b/i18n/README.es.md
@@ -8,10 +8,11 @@
- Construye herramientas empresariales personalizadas en cuestión de minutos y en su propia infraestructura.
+ Construye herramientas empresariales personalizadas en cuestión de minutos y en tu propia infraestructura.
- Budibase es una plataforma de código bajo de código abierto, que ayuda a desarrolladores y profesionales de TI a crear, automatizar y enviar aplicaciones empresariales personalizadas en cuestión de minutos y en su propia infraestructura
+ Budibase es una plataforma low code de código abierto, que ayuda a desarrolladores y profesionales de TI a crear y
+automatizar aplicaciones personalizadas en cuestión de minutos
@@ -20,7 +21,7 @@
-
+
@@ -30,9 +31,6 @@
-
-
-
@@ -43,130 +41,213 @@
+
+## ✨ Caracteristicas
-## ✨ Features
-When other platforms chose the closed source route, we decided to go open source. When other platforms chose cloud builders, we decided a local builder offered the better developer experience. We like to do things differently at Budibase.
+### Construir aplicaciones reales
+Con Budibase podras construir aplicaciones de pagina unica de gran rendimiento. Ademas, puedes hacerlas con un diseño
+adaptativo para darles a tus usuarios una gran experiencia.
+
-- **Build and ship real software.** Unlike other platforms, with Budibase you build and ship single page applications. Budibase applications have performance baked in and can be designed responsively, providing your users with a great experience.
+### Codigo abierto y ampliable
+Budibase es de codigo abierto con licencia GPL v3. Puedes ampliarlo o modificarlo para adaptarlo a tus necesidades y preferencias.
-- **Open source and extensable.** Budibase is open-source. The builder is licensed AGPL v3, the server is GPL v3, and the client is MPL. This should fill you with confidence that Budibase will always be around. You can also code against Budibase or fork it and make changes as you please, providing a developer-friendly experience.
+De esta manera proveemos una buena experiencia para el desarrollador asi como establecemos la confianza de que Budibase siempre estara funcional.
+
-- **Load data or start from scratch.** Budibase pulls in data from multiple sources, including MongoDB, CouchDB, PostgreSQL, mySQL, Airtable, Google Sheets, S3, DyanmoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no data sources. [Request new data sources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
+### Cargar informacion o empezar desde cero
+Budibase permite importar datos desde multiples fuentes, entre las que estan incluidas: MondoDB, CouchDB, PostgreSQL, MySQL,
+Airtable, S3, DynamoDB o API REST.
-- **Design and build apps with powerful pre-made components.** Budibase comes out of the box with beautifully designed, powerful components which you can use like building blocks to build your UI. We also expose a lot of your favourite CSS styling options so you can go that extra creative mile. [Request new components](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
-
-- **Automate processes, integrate with other tools, and connect to webhooks.** Save time by automating manual processes and workflows. From connecting to webhooks, to automating emails, simply tell Budibase what to do and let it work for you. You can easily [create new automations for Budibase here](https://github.com/Budibase/automations) or [request new integrations here](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
-
-- **Cloud hosting and self-hosting.** Users can self-host (see below), or host their apps with Budibase. Currently, our cloud hosting offering is limited to the free tier but we aim to change this in the future. For heavy usage, we advise users to self-host.
+O si lo prefieres, con Budibase puedes empezar desde cero y construir tus propias aplicaciones
+sin necesidad de herramientas externas.
+[Sugerir fuente de datos](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
-
+
+
+### Diseña y construye aplicaciones con componentes profesionales prediseñados
-## ⌛ Status
-- [x] Alpha: We are demoing Budibase to users and receiving feedback
-- [x] Private Beta: We are testing Budibase with a closed set of customers
-- [x] Public Beta: Anyone can [sign-up and use Budibase](https://portal.budi.live/signup).
-- [ ] Official Launch
+Budibase incorpora componentes profesionales prediseñados que podras usar de manera facil e intuitiva
+como bloques de construccion para la interfaz de tu aplicacion.
-Watch "releases" of this repo to get notified of major updates, and give the star button a click whilst you're there.
+Tambien mostramos gran parte del CSS para que puedas adaptar los componentes a tus diseños.
+[Sugerir componente](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
-
+
+
-### Stargazers over time
+### Procesos automatizados, integra tu aplicacion con otras herramientas y conectala a eventos webhook
+
+Ahorra tiempo automatizando flujos de trabajo y procesos manuales. Podras desde conectar eventos webhook hasta automatizar emails,
+simplemente dile a Budibase que hacer y deja que el haga el trabajo por ti.
+[Crear nuevos procesos automatizados](https://github.com/Budibase/automations) o [Sugerir proceso automatizado](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
+
+
+
+
+
+
+### Tus herramientas favoritas
+
+Budibase integra un gran numero de herramientas que te permitiran construir tus aplicaciones ajustandose a tus preferencias.
+
+
+
+
+
+
+### Un paraiso para administradores
+
+Puedes albergar Budibase en tu propia infraestructura y gestionar globalmente usuarios, incorporaciones, SMTP, aplicaciones,
+grupos, diseños de temas, etc.
+
+Tambien puedes gestionar los usuarios y grupos, o delegar en personas asignadas para ello, desde nuestra aplicacion sin
+mucho esfuerzo.
+
+Budibase is made to scale. With Budibase, you can self-host on your own infrastructure and globally manage users, onboarding, SMTP, apps, groups, theming and more. You can also provide users/groups with an app portal and disseminate user-management to the group manager.
+
+- Video Promocional: https://youtu.be/xoljVpty_Kw
+
+
+
+---
+
+
+
+
+## Budibase API Publica
+
+Como todo lo que construimos en Budibase, nuestra nueva API publica es facil de usar, flexible e introduce nueva ampliacion
+del sistema. Budibase API ofrece:
+- Uso de Budibase como backend
+- Interoperabilidad
+
+#### Documentacion
+
+Puedes aprender mas acerca de Budibase API en los siguientes documentos:
+- [Documentacion general](https://docs.budibase.com/docs/public-api) : Como optener tu clave para la API, usar Insomnia y Postman
+- [API Interactiva](https://docs.budibase.com/reference/post_applications) : Aprende como trabajar con la API
+
+#### Guias
+
+- [Construye una aplicacion con Budibase y Next.js](https://budibase.com/blog/building-a-crud-app-with-budibase-and-next.js/)
+
+
+
+
+
+
+
+
+## 🏁 Comenzar con Budibase
+
+Puedes alojar Budibase en tu propia infraestructura con Docker, Kubernetes o Digital Ocean; o usa Budibase en la nube si
+quieres empezar a crear tus aplicaciones rapidamente y sin ningun tipo de preocupacion.
+
+### [Comenzar con Budibase self-hosting](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)
+
+
+### [Comenzar con Budibase en la nube](https://budibase.com)
+
+
+
+## 🎓 Aprende a usar Budibase
+
+Aqui tienes la [documentacion de Budibase](https://docs.budibase.com/docs).
+
+
+
+
+
+## 💬 Comunidad
+
+Te invitamos a que te unas a nuestra comunidad de Budibase, alli podras hacer las preguntas que quieras, ayudar a otras
+personas o tener una charla entretenida con otros usuarios de Budibase.
+[Acceder a la comunidad de Budibase](https://github.com/Budibase/budibase/discussions)
+
+
+
+## ❗ Codigo de conducta
+
+Budibase presta especial atencion en acoger a personas de toda diversidad y ofrecer un entorno de respeto mutuo. Asi mismo
+esperamos lo mismo de nuestra comunidad, por favor lee el
+[**Codigo de conducta**](https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md).
+
+
+
+
+
+## 🙌 Contribuir en Budibase
+
+Desde comunicar un bug a solventar un error en el codigo, toda contribucion es apreciada y bienvenida. Si estas planeando
+implementar una nueva funcionalidad o un realizar un cambio en la API, por favor crea un [nuevo mensaje aqui](https://github.com/Budibase/budibase/issues),
+de esta manera nos encargaremos que tu trabajo no sea en vano.
+
+Aqui tienes instrucciones de como configurar tu entorno Budibase para [Debian](https://github.com/Budibase/budibase/tree/HEAD/docs/DEV-SETUP-DEBIAN.md)
+y [MacOSX](https://github.com/Budibase/budibase/tree/HEAD/docs/DEV-SETUP-MACOSX.md)
+
+### No estas seguro por donde empezar?
+Un buen lugar para empezar a contribuir con nosotros es [aqui](https://github.com/Budibase/budibase/projects/22).
+
+### Organizacion del repositorio
+
+Budibase es un repositorio unico gestionado por Lerna. Lerna construye y publica los paquetes de Budibase sincronizandolos
+cada ves que se realiza un cambio. A rasgos generales, estos son los paquetes que conforman Budibase:
+
+- [packages/builder](https://github.com/Budibase/budibase/tree/HEAD/packages/builder) - contiene el codigo del builder de la parte cliente, esta es una aplicacion svelte.
+
+- [packages/client](https://github.com/Budibase/budibase/tree/HEAD/packages/client) - Este modulo se ejecuta en el browser y es el responsable de leer definiciones JSON y crear aplicaciones web en el momento.
+
+- [packages/server](https://github.com/Budibase/budibase/tree/HEAD/packages/server) - La parte servidor de Budibase. Esta aplicacion Koa es responsable de suministrar lo necesario al builder para asi generar las aplicaciones Budibase. Tambien provee una API para interaccionar con la base de datos y el almacenamiento de ficheros.
+
+Para mas informacion, por favor lee el siguiente documento [CONTRIBUTING.md](https://github.com/Budibase/budibase/blob/HEAD/docs/CONTRIBUTING.md)
+
+
+
+
+## 📝 Licencia
+
+Budibase es open-source, licenciado como [GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html). El cliente y las librerias
+de componentes estan licenciadas como [MPL](https://directory.fsf.org/wiki/License:MPL-2.0) - de esta manera, puedes licenciar
+como tu quieras las aplicaciones que construyas.
+
+
+
+## ⭐ Historia de nuestros Stargazers
[![Stargazers over time](https://starchart.cc/Budibase/budibase.svg)](https://starchart.cc/Budibase/budibase)
-If you are having issues between updates of the builder, please use the guide [here](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md#troubleshooting) to clear down your environment.
+Si estas teniendo problemas con el builder despues de actualizar, por favor [lee esta guia](https://github.com/Budibase/budibase/blob/HEAD/docs/CONTRIBUTING.md#troubleshooting) to clear down your environment.
+
-## 🏁 Getting Started with Budibase
+## Contribuidores ✨
-The Budibase builder runs in Electron, on Mac, PC and Linux. Follow the steps below to get started:
-- [ ] [Sign-up to Budibase](https://portal.budi.live/signup)
-- [ ] Create a username and password
-- [ ] Copy your API key
-- [ ] Download Budibase
-- [ ] Open Budibase and enter your API key
-
-[Here is a guided tutorial](https://docs.budibase.com/tutorial/tutorial-signing-up) if you need extra help.
-
-
-## 🤖 Self-hosting
-
-Budibase wants to make sure anyone can use the tools we develop and we know a lot of people need to be able to host the apps they make on their own systems - that is why we've decided to try and make self hosting as easy as possible!
-
-Currently, you can host your apps using Docker or Digital Ocean. The documentation for self-hosting can be found [here](https://docs.budibase.com/docs/hosting-methods).
-
-[![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/droplets/new?onboarding_origin=marketplace&i=09038e&fleetUuid=bb04f9c8-1de8-4687-b2ae-1d5177a0535b&appId=77729671&type=applications&size=s-4vcpu-8gb®ion=nyc1&refcode=0caaa6085a82&image=budibase-20-04)
-
-
-## 🎓 Learning Budibase
-
-The Budibase [documentation lives here](https://docs.budibase.com).
-
-You can also follow a quick tutorial on [how to build a CRM with Budibase](https://docs.budibase.com/tutorial/tutorial-introduction)
-
-
-## Roadmap
-
-Checkout our [Public Roadmap](https://github.com/Budibase/budibase/projects/10). If you would like to discuss some of the items on the roadmap, please feel to reach out on [Discord](https://discord.gg/rCYayfe), or via [Github discussions](https://github.com/Budibase/budibase/discussions)
-
-
-## ❗ Code of Conduct
-
-Budibase is dedicated to providing a welcoming, diverse, and harrassment-free experience for everyone. We expect everyone in the Budibase community to abide by our [**Code of Conduct**](https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md). Please read it.
-
-## 🙌 Contributing to Budibase
-
-From opening a bug report to creating a pull request: every contribution is appreciated and welcomed. If you're planning to implement a new feature or change the API please create an issue first. This way we can ensure your work is not in vain.
-
-### Not Sure Where to Start?
-A good place to start contributing, is the [First time issues project](https://github.com/Budibase/budibase/projects/22).
-
-### How the repository is organized
-Budibase is a monorepo managed by lerna. Lerna manages the building and publishing of the budibase packages. At a high level, here are the packages that make up Budibase.
-
-- [packages/builder](https://github.com/Budibase/budibase/tree/HEAD/packages/builder) - contains code for the budibase builder client side svelte application.
-
-- [packages/client](https://github.com/Budibase/budibase/tree/HEAD/packages/client) - A module that runs in the browser responsible for reading JSON definition and creating living, breathing web apps from it.
-
-- [packages/server](https://github.com/Budibase/budibase/tree/HEAD/packages/server) - The budibase server. This Koa app is responsible for serving the JS for the builder and budibase apps, as well as providing the API for interaction with the database and file system.
-
-For more information, see [CONTRIBUTING.md](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md)
-
-## 📝 License
-
-Budibase is open-source. The builder is licensed [AGPL v3](https://www.gnu.org/licenses/agpl-3.0.en.html), the server is licensed [GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html), and the client is licensed [MPL](https://directory.fsf.org/wiki/License:MPL-2.0).
-
-## 💬 Get in touch
-
-If you have a question or would like to talk with other Budibase users, please hop over to [Github discussions](https://github.com/Budibase/budibase/discussions) or join our Discord server:
-
-[Discord chatroom](https://discord.gg/rCYayfe)
-
-![Discord Shield](https://discordapp.com/api/guilds/733030666647765003/widget.png?style=shield)
-
-
-## Contributors ✨
-
-Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
+Queremos prestar un especial agradecimiento a nuestra maravillosa gente ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
@@ -179,14 +260,18 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Michael Shanks 📖 💻 ⚠️
Kevin Åberg Kultalahti 📖 💻 ⚠️
Joe 📖 💻 🖋 🎨
- Conor_Mack 💻 ⚠️
+ Rory Powell 💻 📖 ⚠️
+ Peter Clement 💻 📖 ⚠️
+ Conor_Mack 💻 ⚠️
pngwn 💻 ⚠️
HugoLd 💻
victoriasloan 💻
yashank09 💻
SOVLOOKUP 💻
+ seoulaja 🌍
+ Maurits Lourens ⚠️ 💻
@@ -195,4 +280,5 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
-This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
+Este proyecto sigue las especificaciones de [all-contributors](https://github.com/all-contributors/all-contributors).
+Todo tipo de contribuciones son agradecidas!
diff --git a/lerna.json b/lerna.json
index 33b8ef3fc1..82fca15c49 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,5 +1,5 @@
{
- "version": "1.1.22-alpha.0",
+ "version": "1.2.20-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 7aab95245c..4e20345508 100644
--- a/packages/backend-core/package.json
+++ b/packages/backend-core/package.json
@@ -1,6 +1,6 @@
{
"name": "@budibase/backend-core",
- "version": "1.1.22-alpha.0",
+ "version": "1.2.20-alpha.1",
"description": "Budibase backend core libraries used in server and worker",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
@@ -20,13 +20,14 @@
"test:watch": "jest --watchAll"
},
"dependencies": {
- "@budibase/types": "^1.1.22-alpha.0",
+ "@budibase/types": "1.2.20-alpha.1",
"@techpass/passport-openidconnect": "0.3.2",
"aws-sdk": "2.1030.0",
"bcrypt": "5.0.1",
"dotenv": "16.0.1",
"emitter-listener": "1.1.2",
"ioredis": "4.28.0",
+ "joi": "17.6.0",
"jsonwebtoken": "8.5.1",
"koa-passport": "4.1.4",
"lodash": "4.17.21",
diff --git a/packages/backend-core/src/auth.js b/packages/backend-core/src/auth.js
index b60144a0de..d39b8426fb 100644
--- a/packages/backend-core/src/auth.js
+++ b/packages/backend-core/src/auth.js
@@ -18,6 +18,10 @@ const {
ssoCallbackUrl,
csrf,
internalApi,
+ adminOnly,
+ builderOnly,
+ builderOrAdmin,
+ joiValidator,
} = require("./middleware")
const { invalidateUser } = require("./cache/user")
@@ -173,4 +177,8 @@ module.exports = {
refreshOAuthToken,
updateUserOAuth,
ssoCallbackUrl,
+ adminOnly,
+ builderOnly,
+ builderOrAdmin,
+ 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/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/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/environment.ts b/packages/backend-core/src/environment.ts
index 37804b31a6..0348d921ab 100644
--- a/packages/backend-core/src/environment.ts
+++ b/packages/backend-core/src/environment.ts
@@ -55,6 +55,8 @@ const env = {
DEFAULT_LICENSE: process.env.DEFAULT_LICENSE,
SERVICE: process.env.SERVICE || "budibase",
MEMORY_LEAK_CHECK: process.env.MEMORY_LEAK_CHECK || false,
+ LOG_LEVEL: process.env.LOG_LEVEL,
+ SESSION_UPDATE_PERIOD: process.env.SESSION_UPDATE_PERIOD,
DEPLOYMENT_ENVIRONMENT:
process.env.DEPLOYMENT_ENVIRONMENT || "docker-compose",
_set(key: any, value: any) {
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/processors/PosthogProcessor.ts b/packages/backend-core/src/events/processors/PosthogProcessor.ts
index eb12db1dc4..9d68d3919a 100644
--- a/packages/backend-core/src/events/processors/PosthogProcessor.ts
+++ b/packages/backend-core/src/events/processors/PosthogProcessor.ts
@@ -5,6 +5,22 @@ import env from "../../environment"
import * as context from "../../context"
const pkg = require("../../../package.json")
+const EXCLUDED_EVENTS: Event[] = [
+ Event.USER_UPDATED,
+ Event.EMAIL_SMTP_UPDATED,
+ Event.AUTH_SSO_UPDATED,
+ Event.APP_UPDATED,
+ Event.ROLE_UPDATED,
+ Event.DATASOURCE_UPDATED,
+ Event.QUERY_UPDATED,
+ Event.TABLE_UPDATED,
+ Event.VIEW_UPDATED,
+ Event.VIEW_FILTER_UPDATED,
+ Event.VIEW_CALCULATION_UPDATED,
+ Event.AUTOMATION_TRIGGER_UPDATED,
+ Event.USER_GROUP_UPDATED,
+]
+
export default class PosthogProcessor implements EventProcessor {
posthog: PostHog
@@ -21,6 +37,11 @@ export default class PosthogProcessor implements EventProcessor {
properties: BaseEvent,
timestamp?: string | number
): Promise {
+ // don't send excluded events
+ if (EXCLUDED_EVENTS.includes(event)) {
+ return
+ }
+
properties.version = pkg.version
properties.service = env.SERVICE
properties.environment = identity.environment
diff --git a/packages/backend-core/src/events/processors/tests/PosthogProcessor.spec.ts b/packages/backend-core/src/events/processors/tests/PosthogProcessor.spec.ts
new file mode 100644
index 0000000000..4a6d55b272
--- /dev/null
+++ b/packages/backend-core/src/events/processors/tests/PosthogProcessor.spec.ts
@@ -0,0 +1,40 @@
+import PosthogProcessor from "../PosthogProcessor"
+import { Event, IdentityType, Hosting } from "@budibase/types"
+
+const newIdentity = () => {
+ return {
+ id: "test",
+ type: IdentityType.USER,
+ hosting: Hosting.SELF,
+ environment: "test",
+ }
+}
+
+describe("PosthogProcessor", () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ })
+
+ describe("processEvent", () => {
+ it("processes event", () => {
+ const processor = new PosthogProcessor("test")
+
+ const identity = newIdentity()
+ const properties = {}
+
+ processor.processEvent(Event.APP_CREATED, identity, properties)
+
+ expect(processor.posthog.capture).toHaveBeenCalledTimes(1)
+ })
+
+ it("honours exclusions", () => {
+ const processor = new PosthogProcessor("test")
+
+ const identity = newIdentity()
+ const properties = {}
+
+ processor.processEvent(Event.AUTH_SSO_UPDATED, identity, properties)
+ expect(processor.posthog.capture).toHaveBeenCalledTimes(0)
+ })
+ })
+})
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/events/publishers/license.ts b/packages/backend-core/src/events/publishers/license.ts
index 44dafd84ce..1adc71652e 100644
--- a/packages/backend-core/src/events/publishers/license.ts
+++ b/packages/backend-core/src/events/publishers/license.ts
@@ -20,12 +20,6 @@ export async function downgraded(license: License) {
await publishEvent(Event.LICENSE_DOWNGRADED, properties)
}
-// TODO
-export async function updated(license: License) {
- const properties: LicenseUpdatedEvent = {}
- await publishEvent(Event.LICENSE_UPDATED, properties)
-}
-
// TODO
export async function activated(license: License) {
const properties: LicenseActivatedEvent = {}
diff --git a/packages/backend-core/src/featureFlags/index.js b/packages/backend-core/src/featureFlags/index.js
index c050cbdfef..103ac4df59 100644
--- a/packages/backend-core/src/featureFlags/index.js
+++ b/packages/backend-core/src/featureFlags/index.js
@@ -50,4 +50,5 @@ exports.getTenantFeatureFlags = tenantId => {
exports.FeatureFlag = {
LICENSING: "LICENSING",
GOOGLE_SHEETS: "GOOGLE_SHEETS",
+ USER_GROUPS: "USER_GROUPS",
}
diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts
index ab89eed3b2..e585d4b6c3 100644
--- a/packages/backend-core/src/index.ts
+++ b/packages/backend-core/src/index.ts
@@ -3,17 +3,19 @@ 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"
import tenancy from "./tenancy"
import featureFlags from "./featureFlags"
-import sessions from "./security/sessions"
+import * as sessions from "./security/sessions"
import deprovisioning from "./context/deprovision"
import auth from "./auth"
import constants from "./constants"
import * as dbConstants from "./db/constants"
import logging from "./logging"
+import pino from "./pino"
// mimic the outer package exports
import * as db from "./pkg/db"
@@ -51,6 +53,8 @@ const core = {
installation,
errors,
logging,
+ roles,
+ ...pino,
...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/worker/src/middleware/adminOnly.js b/packages/backend-core/src/middleware/adminOnly.js
similarity index 100%
rename from packages/worker/src/middleware/adminOnly.js
rename to packages/backend-core/src/middleware/adminOnly.js
diff --git a/packages/backend-core/src/middleware/authenticated.js b/packages/backend-core/src/middleware/authenticated.ts
similarity index 60%
rename from packages/backend-core/src/middleware/authenticated.js
rename to packages/backend-core/src/middleware/authenticated.ts
index 4e6e0b7ba2..3406b00812 100644
--- a/packages/backend-core/src/middleware/authenticated.js
+++ b/packages/backend-core/src/middleware/authenticated.ts
@@ -1,28 +1,39 @@
-const { Cookies, Headers } = require("../constants")
-const { getCookie, clearCookie, openJwt } = require("../utils")
-const { getUser } = require("../cache/user")
-const { getSession, updateSessionTTL } = require("../security/sessions")
-const { buildMatcherRegex, matches } = require("./matchers")
-const env = require("../environment")
-const { SEPARATOR } = require("../db/constants")
-const { ViewNames } = require("../db/utils")
-const { queryGlobalView } = require("../db/views")
-const { getGlobalDB, doInTenant } = require("../tenancy")
-const { decrypt } = require("../security/encryption")
+import { Cookies, Headers } from "../constants"
+import { getCookie, clearCookie, openJwt } from "../utils"
+import { getUser } from "../cache/user"
+import { getSession, updateSessionTTL } from "../security/sessions"
+import { buildMatcherRegex, matches } from "./matchers"
+import { SEPARATOR } from "../db/constants"
+import { ViewNames } from "../db/utils"
+import { queryGlobalView } from "../db/views"
+import { getGlobalDB, doInTenant } from "../tenancy"
+import { decrypt } from "../security/encryption"
const identity = require("../context/identity")
+const env = require("../environment")
-function finalise(
- ctx,
- { authenticated, user, internal, version, publicEndpoint } = {}
-) {
- ctx.publicEndpoint = publicEndpoint || false
- ctx.isAuthenticated = authenticated || false
- ctx.user = user
- ctx.internal = internal || false
- ctx.version = version
+const ONE_MINUTE = env.SESSION_UPDATE_PERIOD || 60 * 1000
+
+interface FinaliseOpts {
+ authenticated?: boolean
+ internal?: boolean
+ publicEndpoint?: boolean
+ version?: string
+ user?: any
}
-async function checkApiKey(apiKey, populateUser) {
+function timeMinusOneMinute() {
+ return new Date(Date.now() - ONE_MINUTE).toISOString()
+}
+
+function finalise(ctx: any, opts: FinaliseOpts = {}) {
+ ctx.publicEndpoint = opts.publicEndpoint || false
+ ctx.isAuthenticated = opts.authenticated || false
+ ctx.user = opts.user
+ ctx.internal = opts.internal || false
+ ctx.version = opts.version
+}
+
+async function checkApiKey(apiKey: string, populateUser?: Function) {
if (apiKey === env.INTERNAL_API_KEY) {
return { valid: true }
}
@@ -56,10 +67,12 @@ async function checkApiKey(apiKey, populateUser) {
*/
module.exports = (
noAuthPatterns = [],
- opts = { publicAllowed: false, populateUser: null }
+ opts: { publicAllowed: boolean; populateUser?: Function } = {
+ publicAllowed: false,
+ }
) => {
const noAuthOptions = noAuthPatterns ? buildMatcherRegex(noAuthPatterns) : []
- return async (ctx, next) => {
+ return async (ctx: any, next: any) => {
let publicEndpoint = false
const version = ctx.request.headers[Headers.API_VER]
// the path is not authenticated
@@ -71,45 +84,40 @@ module.exports = (
// check the actual user is authenticated first, try header or cookie
const headerToken = ctx.request.headers[Headers.TOKEN]
const authCookie = getCookie(ctx, Cookies.Auth) || openJwt(headerToken)
+ const apiKey = ctx.request.headers[Headers.API_KEY]
+ const tenantId = ctx.request.headers[Headers.TENANT_ID]
let authenticated = false,
user = null,
internal = false
- if (authCookie) {
- let error = null
+ if (authCookie && !apiKey) {
const sessionId = authCookie.sessionId
const userId = authCookie.userId
-
- const session = await getSession(userId, sessionId)
- if (!session) {
- error = "No session found"
- } else {
- try {
- if (opts && opts.populateUser) {
- user = await getUser(
- userId,
- session.tenantId,
- opts.populateUser(ctx)
- )
- } else {
- user = await getUser(userId, session.tenantId)
- }
- user.csrfToken = session.csrfToken
- authenticated = true
- } catch (err) {
- error = err
+ let session
+ try {
+ // getting session handles error checking (if session exists etc)
+ session = await getSession(userId, sessionId)
+ if (opts && opts.populateUser) {
+ user = await getUser(
+ userId,
+ session.tenantId,
+ opts.populateUser(ctx)
+ )
+ } else {
+ user = await getUser(userId, session.tenantId)
}
- }
- if (error) {
- console.error("Auth Error", error)
+ user.csrfToken = session.csrfToken
+ if (session?.lastAccessedAt < timeMinusOneMinute()) {
+ // make sure we denote that the session is still in use
+ await updateSessionTTL(session)
+ }
+ authenticated = true
+ } catch (err: any) {
+ authenticated = false
+ console.error("Auth Error", err?.message || err)
// remove the cookie as the user does not exist anymore
clearCookie(ctx, Cookies.Auth)
- } else {
- // make sure we denote that the session is still in use
- await updateSessionTTL(session)
}
}
- const apiKey = ctx.request.headers[Headers.API_KEY]
- const tenantId = ctx.request.headers[Headers.TENANT_ID]
// this is an internal request, no user made it
if (!authenticated && apiKey) {
const populateUser = opts.populateUser ? opts.populateUser(ctx) : null
@@ -127,7 +135,7 @@ module.exports = (
}
if (!user && tenantId) {
user = { tenantId }
- } else {
+ } else if (user) {
delete user.password
}
// be explicit
@@ -142,7 +150,7 @@ module.exports = (
} else {
return next()
}
- } catch (err) {
+ } catch (err: any) {
// invalid token, clear the cookie
if (err && err.name === "JsonWebTokenError") {
clearCookie(ctx, Cookies.Auth)
diff --git a/packages/worker/src/middleware/builderOnly.js b/packages/backend-core/src/middleware/builderOnly.js
similarity index 100%
rename from packages/worker/src/middleware/builderOnly.js
rename to packages/backend-core/src/middleware/builderOnly.js
diff --git a/packages/worker/src/middleware/builderOrAdmin.js b/packages/backend-core/src/middleware/builderOrAdmin.js
similarity index 100%
rename from packages/worker/src/middleware/builderOrAdmin.js
rename to packages/backend-core/src/middleware/builderOrAdmin.js
diff --git a/packages/backend-core/src/middleware/index.js b/packages/backend-core/src/middleware/index.js
index 1721d56a3c..7e7b8a2931 100644
--- a/packages/backend-core/src/middleware/index.js
+++ b/packages/backend-core/src/middleware/index.js
@@ -9,7 +9,10 @@ const tenancy = require("./tenancy")
const internalApi = require("./internalApi")
const datasourceGoogle = require("./passport/datasource/google")
const csrf = require("./csrf")
-
+const adminOnly = require("./adminOnly")
+const builderOrAdmin = require("./builderOrAdmin")
+const builderOnly = require("./builderOnly")
+const joiValidator = require("./joi-validator")
module.exports = {
google,
oidc,
@@ -25,4 +28,8 @@ module.exports = {
google: datasourceGoogle,
},
csrf,
+ adminOnly,
+ builderOnly,
+ builderOrAdmin,
+ joiValidator,
}
diff --git a/packages/worker/src/middleware/joi-validator.js b/packages/backend-core/src/middleware/joi-validator.js
similarity index 81%
rename from packages/worker/src/middleware/joi-validator.js
rename to packages/backend-core/src/middleware/joi-validator.js
index 1686b0e727..748ccebd89 100644
--- a/packages/worker/src/middleware/joi-validator.js
+++ b/packages/backend-core/src/middleware/joi-validator.js
@@ -1,3 +1,5 @@
+const Joi = require("joi")
+
function validate(schema, property) {
// Return a Koa middleware function
return (ctx, next) => {
@@ -10,6 +12,12 @@ function validate(schema, property) {
} else if (ctx.request[property] != null) {
params = ctx.request[property]
}
+
+ schema = schema.append({
+ createdAt: Joi.any().optional(),
+ updatedAt: Joi.any().optional(),
+ })
+
const { error } = schema.validate(params)
if (error) {
ctx.throw(400, `Invalid ${property} - ${error.message}`)
diff --git a/packages/backend-core/src/migrations/definitions.ts b/packages/backend-core/src/migrations/definitions.ts
index 745c8718c9..34ec0f0cad 100644
--- a/packages/backend-core/src/migrations/definitions.ts
+++ b/packages/backend-core/src/migrations/definitions.ts
@@ -37,4 +37,8 @@ export const DEFINITIONS: MigrationDefinition[] = [
type: MigrationType.INSTALLATION,
name: MigrationName.EVENT_INSTALLATION_BACKFILL,
},
+ {
+ type: MigrationType.GLOBAL,
+ name: MigrationName.GLOBAL_INFO_SYNC_USERS,
+ },
]
diff --git a/packages/backend-core/src/pino.js b/packages/backend-core/src/pino.js
new file mode 100644
index 0000000000..69962b3841
--- /dev/null
+++ b/packages/backend-core/src/pino.js
@@ -0,0 +1,11 @@
+const env = require("./environment")
+
+exports.pinoSettings = () => ({
+ prettyPrint: {
+ levelFirst: true,
+ },
+ level: env.LOG_LEVEL || "error",
+ autoLogging: {
+ ignore: req => req.url.includes("/health"),
+ },
+})
diff --git a/packages/backend-core/src/security/roles.js b/packages/backend-core/src/security/roles.js
index 7c57cadcbf..30869da68e 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
}
@@ -202,15 +203,24 @@ exports.getAllRoles = async appId => {
if (appId) {
return doWithDB(appId, internal)
} else {
- return internal(getAppDB())
+ let appDB
+ try {
+ appDB = getAppDB()
+ } catch (error) {
+ // We don't have any apps, so we'll just use the built-in roles
+ }
+ return internal(appDB)
}
async function internal(db) {
- const body = await db.allDocs(
- getRoleParams(null, {
- include_docs: true,
- })
- )
- let roles = body.rows.map(row => row.doc)
+ let roles = []
+ if (db) {
+ const body = await db.allDocs(
+ getRoleParams(null, {
+ include_docs: true,
+ })
+ )
+ roles = body.rows.map(row => row.doc)
+ }
const builtinRoles = exports.getBuiltinRoles()
// need to combine builtin with any DB record of them (for sake of permissions)
diff --git a/packages/backend-core/src/security/sessions.js b/packages/backend-core/src/security/sessions.js
deleted file mode 100644
index 8874b47469..0000000000
--- a/packages/backend-core/src/security/sessions.js
+++ /dev/null
@@ -1,95 +0,0 @@
-const redis = require("../redis/init")
-const { v4: uuidv4 } = require("uuid")
-
-// a week in seconds
-const EXPIRY_SECONDS = 86400 * 7
-
-async function getSessionsForUser(userId) {
- const client = await redis.getSessionClient()
- const sessions = await client.scan(userId)
- return sessions.map(session => session.value)
-}
-
-function makeSessionID(userId, sessionId) {
- return `${userId}/${sessionId}`
-}
-
-async function invalidateSessions(userId, sessionIds = null) {
- try {
- let sessions = []
-
- // If no sessionIds, get all the sessions for the user
- if (!sessionIds) {
- sessions = await getSessionsForUser(userId)
- sessions.forEach(
- session =>
- (session.key = makeSessionID(session.userId, session.sessionId))
- )
- } else {
- // use the passed array of sessionIds
- sessions = Array.isArray(sessionIds) ? sessionIds : [sessionIds]
- sessions = sessions.map(sessionId => ({
- key: makeSessionID(userId, sessionId),
- }))
- }
-
- const client = await redis.getSessionClient()
- const promises = []
- for (let session of sessions) {
- promises.push(client.delete(session.key))
- }
- await Promise.all(promises)
- } catch (err) {
- console.error(`Error invalidating sessions: ${err}`)
- }
-}
-
-exports.createASession = async (userId, session) => {
- // invalidate all other sessions
- await invalidateSessions(userId)
-
- const client = await redis.getSessionClient()
- const sessionId = session.sessionId
- if (!session.csrfToken) {
- session.csrfToken = uuidv4()
- }
- session = {
- createdAt: new Date().toISOString(),
- lastAccessedAt: new Date().toISOString(),
- ...session,
- userId,
- }
- await client.store(makeSessionID(userId, sessionId), session, EXPIRY_SECONDS)
-}
-
-exports.updateSessionTTL = async session => {
- const client = await redis.getSessionClient()
- const key = makeSessionID(session.userId, session.sessionId)
- session.lastAccessedAt = new Date().toISOString()
- await client.store(key, session, EXPIRY_SECONDS)
-}
-
-exports.endSession = async (userId, sessionId) => {
- const client = await redis.getSessionClient()
- await client.delete(makeSessionID(userId, sessionId))
-}
-
-exports.getSession = async (userId, sessionId) => {
- try {
- const client = await redis.getSessionClient()
- return client.get(makeSessionID(userId, sessionId))
- } catch (err) {
- // if can't get session don't error, just don't return anything
- console.error(err)
- return null
- }
-}
-
-exports.getAllSessions = async () => {
- const client = await redis.getSessionClient()
- const sessions = await client.scan()
- return sessions.map(session => session.value)
-}
-
-exports.getUserSessions = getSessionsForUser
-exports.invalidateSessions = invalidateSessions
diff --git a/packages/backend-core/src/security/sessions.ts b/packages/backend-core/src/security/sessions.ts
new file mode 100644
index 0000000000..284adbcd1f
--- /dev/null
+++ b/packages/backend-core/src/security/sessions.ts
@@ -0,0 +1,119 @@
+const redis = require("../redis/init")
+const { v4: uuidv4 } = require("uuid")
+const { logWarn } = require("../logging")
+const env = require("../environment")
+
+interface Session {
+ key: string
+ userId: string
+ sessionId: string
+ lastAccessedAt: string
+ createdAt: string
+ csrfToken?: string
+ value: string
+}
+
+type SessionKey = { key: string }[]
+
+// a week in seconds
+const EXPIRY_SECONDS = 86400 * 7
+
+function makeSessionID(userId: string, sessionId: string) {
+ return `${userId}/${sessionId}`
+}
+
+export async function getSessionsForUser(userId: string) {
+ if (!userId) {
+ console.trace("Cannot get sessions for undefined userId")
+ return []
+ }
+ const client = await redis.getSessionClient()
+ const sessions = await client.scan(userId)
+ return sessions.map((session: Session) => session.value)
+}
+
+export async function invalidateSessions(
+ userId: string,
+ opts: { sessionIds?: string[]; reason?: string } = {}
+) {
+ try {
+ const reason = opts?.reason || "unknown"
+ let sessionIds: string[] = opts.sessionIds || []
+ let sessions: SessionKey
+
+ // If no sessionIds, get all the sessions for the user
+ if (sessionIds.length === 0) {
+ sessions = await getSessionsForUser(userId)
+ sessions.forEach(
+ (session: any) =>
+ (session.key = makeSessionID(session.userId, session.sessionId))
+ )
+ } else {
+ // use the passed array of sessionIds
+ sessionIds = Array.isArray(sessionIds) ? sessionIds : [sessionIds]
+ sessions = sessionIds.map((sessionId: string) => ({
+ key: makeSessionID(userId, sessionId),
+ }))
+ }
+
+ if (sessions && sessions.length > 0) {
+ const client = await redis.getSessionClient()
+ const promises = []
+ for (let session of sessions) {
+ promises.push(client.delete(session.key))
+ }
+ if (!env.isTest()) {
+ logWarn(
+ `Invalidating sessions for ${userId} (reason: ${reason}) - ${sessions
+ .map(session => session.key)
+ .join(", ")}`
+ )
+ }
+ await Promise.all(promises)
+ }
+ } catch (err) {
+ console.error(`Error invalidating sessions: ${err}`)
+ }
+}
+
+export async function createASession(userId: string, session: Session) {
+ // invalidate all other sessions
+ await invalidateSessions(userId, { reason: "creation" })
+
+ const client = await redis.getSessionClient()
+ const sessionId = session.sessionId
+ if (!session.csrfToken) {
+ session.csrfToken = uuidv4()
+ }
+ session = {
+ ...session,
+ createdAt: new Date().toISOString(),
+ lastAccessedAt: new Date().toISOString(),
+ userId,
+ }
+ await client.store(makeSessionID(userId, sessionId), session, EXPIRY_SECONDS)
+}
+
+export async function updateSessionTTL(session: Session) {
+ const client = await redis.getSessionClient()
+ const key = makeSessionID(session.userId, session.sessionId)
+ session.lastAccessedAt = new Date().toISOString()
+ await client.store(key, session, EXPIRY_SECONDS)
+}
+
+export async function endSession(userId: string, sessionId: string) {
+ const client = await redis.getSessionClient()
+ await client.delete(makeSessionID(userId, sessionId))
+}
+
+export async function getSession(userId: string, sessionId: string) {
+ if (!userId || !sessionId) {
+ throw new Error(`Invalid session details - ${userId} - ${sessionId}`)
+ }
+ const client = await redis.getSessionClient()
+ const session = await client.get(makeSessionID(userId, sessionId))
+ if (!session) {
+ throw new Error(`Session not found - ${userId} - ${sessionId}`)
+ }
+ return session
+}
diff --git a/packages/backend-core/src/security/tests/sessions.spec.ts b/packages/backend-core/src/security/tests/sessions.spec.ts
new file mode 100644
index 0000000000..7f01bdcdb7
--- /dev/null
+++ b/packages/backend-core/src/security/tests/sessions.spec.ts
@@ -0,0 +1,12 @@
+import * as sessions from "../sessions"
+
+describe("sessions", () => {
+ describe("getSessionsForUser", () => {
+ it("returns empty when user is undefined", async () => {
+ // @ts-ignore - allow the undefined to be passed
+ const results = await sessions.getSessionsForUser(undefined)
+
+ expect(results).toStrictEqual([])
+ })
+ })
+})
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/src/utils.js b/packages/backend-core/src/utils.js
index cf32539c58..1e143968d9 100644
--- a/packages/backend-core/src/utils.js
+++ b/packages/backend-core/src/utils.js
@@ -10,7 +10,10 @@ const { queryGlobalView } = require("./db/views")
const { Headers, Cookies, MAX_VALID_DATE } = require("./constants")
const env = require("./environment")
const userCache = require("./cache/user")
-const { getUserSessions, invalidateSessions } = require("./security/sessions")
+const {
+ getSessionsForUser,
+ invalidateSessions,
+} = require("./security/sessions")
const events = require("./events")
const tenancy = require("./tenancy")
@@ -178,7 +181,7 @@ exports.platformLogout = async ({ ctx, userId, keepActiveSession }) => {
if (!ctx) throw new Error("Koa context must be supplied to logout.")
const currentSession = exports.getCookie(ctx, Cookies.Auth)
- let sessions = await getUserSessions(userId)
+ let sessions = await getSessionsForUser(userId)
if (keepActiveSession) {
sessions = sessions.filter(
@@ -190,10 +193,8 @@ exports.platformLogout = async ({ ctx, userId, keepActiveSession }) => {
exports.clearCookie(ctx, Cookies.CurrentApp)
}
- await invalidateSessions(
- userId,
- sessions.map(({ sessionId }) => sessionId)
- )
+ const sessionIds = sessions.map(({ sessionId }) => sessionId)
+ await invalidateSessions(userId, { sessionIds, reason: "logout" })
await events.auth.logout()
await userCache.invalidateUser(userId)
}
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/backend-core/tests/utilities/mocks/index.js b/packages/backend-core/tests/utilities/mocks/index.js
index 3dd5c854c0..6aa1c4a54f 100644
--- a/packages/backend-core/tests/utilities/mocks/index.js
+++ b/packages/backend-core/tests/utilities/mocks/index.js
@@ -1,7 +1,9 @@
+const posthog = require("./posthog")
const events = require("./events")
const date = require("./date")
module.exports = {
+ posthog,
date,
events,
}
diff --git a/packages/backend-core/tests/utilities/mocks/posthog.ts b/packages/backend-core/tests/utilities/mocks/posthog.ts
new file mode 100644
index 0000000000..e9cc653ccc
--- /dev/null
+++ b/packages/backend-core/tests/utilities/mocks/posthog.ts
@@ -0,0 +1,7 @@
+jest.mock("posthog-node", () => {
+ return jest.fn().mockImplementation(() => {
+ return {
+ capture: jest.fn(),
+ }
+ })
+})
diff --git a/packages/backend-core/yarn.lock b/packages/backend-core/yarn.lock
index e1f38a798f..9f71691f44 100644
--- a/packages/backend-core/yarn.lock
+++ b/packages/backend-core/yarn.lock
@@ -291,6 +291,18 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
+"@hapi/hoek@^9.0.0":
+ version "9.3.0"
+ resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb"
+ integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==
+
+"@hapi/topo@^5.0.0":
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012"
+ integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==
+ dependencies:
+ "@hapi/hoek" "^9.0.0"
+
"@istanbuljs/load-nyc-config@^1.0.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
@@ -539,6 +551,23 @@
koa "^2.13.4"
node-mocks-http "^1.5.8"
+"@sideway/address@^4.1.3":
+ version "4.1.4"
+ resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.4.tgz#03dccebc6ea47fdc226f7d3d1ad512955d4783f0"
+ integrity sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==
+ dependencies:
+ "@hapi/hoek" "^9.0.0"
+
+"@sideway/formula@^3.0.0":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c"
+ integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==
+
+"@sideway/pinpoint@^2.0.0":
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df"
+ integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==
+
"@sindresorhus/is@^0.14.0":
version "0.14.0"
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
@@ -3193,6 +3222,17 @@ jmespath@0.15.0:
resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217"
integrity sha512-+kHj8HXArPfpPEKGLZ+kB5ONRTCiGQXo8RQYL0hH8t6pWXUBBK5KkkQmTNOwKK4LEsd0yTsgtjJVm4UBSZea4w==
+joi@17.6.0:
+ version "17.6.0"
+ resolved "https://registry.yarnpkg.com/joi/-/joi-17.6.0.tgz#0bb54f2f006c09a96e75ce687957bd04290054b2"
+ integrity sha512-OX5dG6DTbcr/kbMFj0KGYxuew69HPcAE3K/sZpEV2nP6e/j/C0HV+HNiBPCASxdx5T7DMoa0s8UeHWMnb6n2zw==
+ dependencies:
+ "@hapi/hoek" "^9.0.0"
+ "@hapi/topo" "^5.0.0"
+ "@sideway/address" "^4.1.3"
+ "@sideway/formula" "^3.0.0"
+ "@sideway/pinpoint" "^2.0.0"
+
join-component@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/join-component/-/join-component-1.1.0.tgz#b8417b750661a392bee2c2537c68b2a9d4977cd5"
diff --git a/packages/bbui/package.json b/packages/bbui/package.json
index 80489d5b9a..4e6f6fa220 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.22-alpha.0",
+ "version": "1.2.20-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.22-alpha.0",
+ "@budibase/string-templates": "1.2.20-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/Form/Core/InputDropdown.svelte b/packages/bbui/src/Form/Core/InputDropdown.svelte
new file mode 100644
index 0000000000..8865ee3ddc
--- /dev/null
+++ b/packages/bbui/src/Form/Core/InputDropdown.svelte
@@ -0,0 +1,228 @@
+
+
+
+
+
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..28cb2b2a4e
--- /dev/null
+++ b/packages/bbui/src/Form/Core/PickerDropdown.svelte
@@ -0,0 +1,436 @@
+
+
+
+
+
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..bc3e17eff1
--- /dev/null
+++ b/packages/bbui/src/Form/PickerDropdown.svelte
@@ -0,0 +1,134 @@
+
+
+
+
+
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..c9e4e397e2
--- /dev/null
+++ b/packages/bbui/src/List/ListItem.svelte
@@ -0,0 +1,98 @@
+
+
+
+
+ {#if icon}
+
+
+
+ {/if}
+ {#if avatar}
+
+ {/if}
+ {#if title}
+ {title}
+ {/if}
+ {#if subtitle}
+
{subtitle}
+ {/if}
+
+
+
+
+
+
+
diff --git a/packages/bbui/src/Modal/ModalContent.svelte b/packages/bbui/src/Modal/ModalContent.svelte
index 10cd4b10ba..6d609d6f1b 100644
--- a/packages/bbui/src/Modal/ModalContent.svelte
+++ b/packages/bbui/src/Modal/ModalContent.svelte
@@ -106,7 +106,9 @@
{/if}
{#if showCancelButton}
- {cancelText}
+
+ {cancelText}
+
{/if}
{#if showConfirmButton}
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/SetupPanel/AutomationBlockSetup.svelte b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte
index f77374985d..90e7ab661c 100644
--- a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte
+++ b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte
@@ -1,6 +1,7 @@
+
+{#if schemaFields.length && isTestModal}
+
+ {#each schemaFields as [field, schema]}
+
+ {/each}
+
+{/if}
+
+
diff --git a/packages/builder/src/components/backend/DataTable/DataTable.svelte b/packages/builder/src/components/backend/DataTable/DataTable.svelte
index 1f461ebad3..37742626cd 100644
--- a/packages/builder/src/components/backend/DataTable/DataTable.svelte
+++ b/packages/builder/src/components/backend/DataTable/DataTable.svelte
@@ -14,7 +14,13 @@
import Table from "./Table.svelte"
import { TableNames } from "constants"
import CreateEditRow from "./modals/CreateEditRow.svelte"
- import { Pagination, Heading, Body, Layout } from "@budibase/bbui"
+ import {
+ Pagination,
+ Heading,
+ Body,
+ Layout,
+ notifications,
+ } from "@budibase/bbui"
import { fetchData } from "@budibase/frontend-core"
import { API } from "api"
@@ -29,6 +35,13 @@
$: fetch = createFetch(id)
$: hasCols = checkHasCols(schema)
$: hasRows = !!$fetch.rows?.length
+ $: showError($fetch.error)
+
+ const showError = error => {
+ if (error) {
+ notifications.error(error?.message || "Unable to fetch data.")
+ }
+ }
const enrichSchema = schema => {
let tempSchema = { ...schema }
diff --git a/packages/builder/src/components/backend/DataTable/buttons/DeleteRowsButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/DeleteRowsButton.svelte
index 3d662ed556..e70a0aa042 100644
--- a/packages/builder/src/components/backend/DataTable/buttons/DeleteRowsButton.svelte
+++ b/packages/builder/src/components/backend/DataTable/buttons/DeleteRowsButton.svelte
@@ -5,6 +5,7 @@
export let selectedRows
export let deleteRows
+ export let item = "row"
const dispatch = createEventDispatcher()
let modal
@@ -14,12 +15,14 @@
modal?.hide()
dispatch("updaterows")
}
+
+ $: text = `${item}${selectedRows?.length === 1 ? "" : "s"}`
Delete
{selectedRows.length}
- row(s)
+ {text}
Are you sure you want to delete
{selectedRows.length}
- row{selectedRows.length > 1 ? "s" : ""}?
+ {text}?
diff --git a/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/rest/auth/RestAuthenticationModal.svelte b/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/rest/auth/RestAuthenticationModal.svelte
index b754f878ce..f19f2279d9 100644
--- a/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/rest/auth/RestAuthenticationModal.svelte
+++ b/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/rest/auth/RestAuthenticationModal.svelte
@@ -211,7 +211,6 @@
bindings={getAuthBindings()}
on:change={e => {
form.bearer.token = e.detail
- console.log(e.detail)
onFieldChange()
}}
on:blur={() => {
diff --git a/packages/builder/src/components/common/AppLockModal.svelte b/packages/builder/src/components/common/AppLockModal.svelte
index 5ca35f05db..9794e350d9 100644
--- a/packages/builder/src/components/common/AppLockModal.svelte
+++ b/packages/builder/src/components/common/AppLockModal.svelte
@@ -6,6 +6,8 @@
Modal,
notifications,
ProgressCircle,
+ Layout,
+ Body,
} from "@budibase/bbui"
import { auth, apps } from "stores/portal"
import { processStringSync } from "@budibase/string-templates"
@@ -72,62 +74,67 @@
{/if}
-
-
-
- Apps are locked to prevent work from being lost from overlapping changes
- between your team.
-
-
- {#if lockedByYou && getExpiryDuration(app) > 0}
-
- {processStringSync(
- "This lock will expire in {{ duration time 'millisecond' }} from now.",
- {
- time: getExpiryDuration(app),
- }
- )}
-
- {/if}
-
-
- {
- appLockModal.hide()
- }}
- >
- {lockedBy && !lockedByYou ? "Done" : "Cancel"}
-
- {#if lockedByYou}
- {
- releaseLock()
- appLockModal.hide()
- }}
- >
- {#if processing}
-
- {:else}
- Release Lock
- {/if}
-
- {/if}
-
-
-
-
+{#key app}
+
+
+
+
+
+ Apps are locked to prevent work from being lost from overlapping
+ changes between your team.
+
+ {#if lockedByYou && getExpiryDuration(app) > 0}
+
+ {processStringSync(
+ "This lock will expire in {{ duration time 'millisecond' }} from now. This lock will expire in This lock will expire in ",
+ {
+ time: getExpiryDuration(app),
+ }
+ )}
+
+ {/if}
+
+
+ {
+ appLockModal.hide()
+ }}
+ >
+ {lockedBy && !lockedByYou ? "Done" : "Cancel"}
+
+ {#if lockedByYou}
+ {
+ releaseLock()
+ appLockModal.hide()
+ }}
+ >
+ {#if processing}
+
+ {:else}
+ Release Lock
+ {/if}
+
+ {/if}
+
+
+
+
+
+
+{/key}
diff --git a/packages/builder/src/components/common/RoleSelect.svelte b/packages/builder/src/components/common/RoleSelect.svelte
new file mode 100644
index 0000000000..a3f75fd4eb
--- /dev/null
+++ b/packages/builder/src/components/common/RoleSelect.svelte
@@ -0,0 +1,24 @@
+
+
+
role.name}
+ getOptionValue={role => role._id}
+ getOptionColour={role => RoleUtils.getRoleColour(role._id)}
+ {placeholder}
+ {error}
+/>
diff --git a/packages/builder/src/components/common/bindings/BindingPanel.svelte b/packages/builder/src/components/common/bindings/BindingPanel.svelte
index f05f935226..49cbd434cf 100644
--- a/packages/builder/src/components/common/bindings/BindingPanel.svelte
+++ b/packages/builder/src/components/common/bindings/BindingPanel.svelte
@@ -8,6 +8,7 @@
Tab,
Body,
Layout,
+ Button,
} from "@budibase/bbui"
import { createEventDispatcher, onMount } from "svelte"
import {
@@ -15,10 +16,15 @@
decodeJSBinding,
encodeJSBinding,
} from "@budibase/string-templates"
- import { readableToRuntimeBinding } from "builderStore/dataBinding"
+ import {
+ readableToRuntimeBinding,
+ runtimeToReadableBinding,
+ } from "builderStore/dataBinding"
import { handlebarsCompletions } from "constants/completions"
import { addHBSBinding, addJSBinding } from "./utils"
import CodeMirrorEditor from "components/common/CodeMirrorEditor.svelte"
+ import { convertToJS } from "@budibase/string-templates"
+ import { admin } from "stores/portal"
const dispatch = createEventDispatcher()
@@ -62,15 +68,24 @@
}
}
- // Adds a HBS helper to the expression
- const addHelper = helper => {
- hbsValue = addHBSBinding(hbsValue, getCaretPosition(), helper.text)
- updateValue(hbsValue)
+ // Adds a JS/HBS helper to the expression
+ const addHelper = (helper, js) => {
+ let tempVal
+ const pos = getCaretPosition()
+ if (js) {
+ const decoded = decodeJSBinding(jsValue)
+ tempVal = jsValue = encodeJSBinding(
+ addJSBinding(decoded, pos, helper.text, { helper: true })
+ )
+ } else {
+ tempVal = hbsValue = addHBSBinding(hbsValue, pos, helper.text)
+ }
+ updateValue(tempVal)
}
// Adds a data binding to the expression
- const addBinding = binding => {
- if (usingJS) {
+ const addBinding = (binding, { forceJS } = {}) => {
+ if (usingJS || forceJS) {
let js = decodeJSBinding(jsValue)
js = addJSBinding(js, getCaretPosition(), binding.readableBinding)
jsValue = encodeJSBinding(js)
@@ -100,6 +115,26 @@
updateValue(jsValue)
}
+ const convert = () => {
+ const runtime = readableToRuntimeBinding(bindings, hbsValue)
+ const runtimeJs = encodeJSBinding(convertToJS(runtime))
+ jsValue = runtimeToReadableBinding(bindings, runtimeJs)
+ hbsValue = null
+ mode = "JavaScript"
+ addBinding("", { forceJS: true })
+ }
+
+ const getHelperExample = (helper, js) => {
+ let example = helper.example || ""
+ if (js) {
+ example = convertToJS(example).split("\n")[0].split("= ")[1]
+ if (example === "null;") {
+ example = ""
+ }
+ }
+ return example || ""
+ }
+
onMount(() => {
valid = isValid(readableToRuntimeBinding(bindings, value))
})
@@ -135,18 +170,21 @@
{/if}
{/each}
- {#if filteredHelpers?.length && !usingJS}
+ {#if filteredHelpers?.length}
Helpers
{#each filteredHelpers as helper}
- addHelper(helper)}>
+ addHelper(helper, usingJS)}>
{helper.displayText}
{@html helper.description}
-
{helper.example || ""}
+
{getHelperExample(
+ helper,
+ usingJS
+ )}
{/each}
@@ -172,6 +210,11 @@
for more details.
{/if}
+ {#if $admin.isDev}
+
+ Convert to JS
+
+ {/if}
{#if allowJS}
@@ -306,4 +349,8 @@
color: var(--red);
text-decoration: underline;
}
+
+ .convert {
+ padding-top: var(--spacing-m);
+ }
diff --git a/packages/builder/src/components/common/bindings/utils.js b/packages/builder/src/components/common/bindings/utils.js
index 42a3f11677..c7b40604ad 100644
--- a/packages/builder/src/components/common/bindings/utils.js
+++ b/packages/builder/src/components/common/bindings/utils.js
@@ -18,10 +18,14 @@ export function addHBSBinding(value, caretPos, binding) {
return value
}
-export function addJSBinding(value, caretPos, binding) {
+export function addJSBinding(value, caretPos, binding, { helper } = {}) {
binding = typeof binding === "string" ? binding : binding.path
value = value == null ? "" : value
- binding = `$("${binding}")`
+ if (!helper) {
+ binding = `$("${binding}")`
+ } else {
+ binding = `helper.${binding}()`
+ }
if (caretPos.start) {
value =
value.substring(0, caretPos.start) +
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} ?
-
+
+ Preview
+
+
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/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/AppRow.svelte b/packages/builder/src/components/start/AppRow.svelte
index 49f99c9f77..91920073bb 100644
--- a/packages/builder/src/components/start/AppRow.svelte
+++ b/packages/builder/src/components/start/AppRow.svelte
@@ -30,7 +30,7 @@
{/if}
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/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/helpers/featureFlags.js b/packages/builder/src/helpers/featureFlags.js
index 9533abed7e..a0cda8d5fa 100644
--- a/packages/builder/src/helpers/featureFlags.js
+++ b/packages/builder/src/helpers/featureFlags.js
@@ -3,6 +3,7 @@ import { get } from "svelte/store"
export const FEATURE_FLAGS = {
LICENSING: "LICENSING",
+ USER_GROUPS: "USER_GROUPS",
}
export const isEnabled = featureFlag => {
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]/design/[screenId]/_components/AppPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPanel.svelte
index 76118cc9c8..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,24 +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)}
value={$store.selectedScreenId}
on:change={e => store.actions.screens.select(e.detail)}
+ quiet
+ autoWidth
/>
@@ -59,6 +51,7 @@
justify-content: space-between;
align-items: flex-start;
gap: var(--spacing-l);
+ margin: 0 2px;
}
.header-left,
.header-right {
@@ -69,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 abb956c9d3..3c99c90d49 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"
@@ -96,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) {
@@ -179,7 +186,7 @@
$goto("./navigation")
}
} else if (type === "request-add-component") {
- $goto("./components/new")
+ $goto(`./components/${$selectedComponent?._id}/new`)
} else if (type === "highlight-setting") {
store.actions.settings.highlight(data.setting)
@@ -219,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) {
@@ -282,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/[componentId]/_components/navigation/ComponentListPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentListPanel.svelte
index 5b86d4da29..ab776dea5d 100644
--- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentListPanel.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentListPanel.svelte
@@ -9,6 +9,7 @@
import { setContext } from "svelte"
import DNDPositionIndicator from "./DNDPositionIndicator.svelte"
import { DropPosition } from "./dndStore"
+ import { notifications, Button } from "@budibase/bbui"
let scrollRef
@@ -23,7 +24,7 @@
let newOffsets = {}
// Calculate left offset
- const offsetX = bounds.left + bounds.width + scrollLeft - 58
+ const offsetX = bounds.left + bounds.width + scrollLeft - 36
if (offsetX > sidebarWidth) {
newOffsets.left = offsetX - sidebarWidth
} else {
@@ -55,19 +56,25 @@
})
}
+ const onDrop = async () => {
+ try {
+ await dndStore.actions.drop()
+ } catch (error) {
+ console.error(error)
+ notifications.error("Error saving component")
+ }
+ }
+
// Set scroll context so components can invoke scrolling when selected
setContext("scroll", {
scrollTo,
})
- $goto("../new")}
- showExpandIcon
- borderRight
->
+
+
+ $goto("./new")} cta>Add component
+
@@ -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]/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 8097291952..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
@@ -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 ecb020b104..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 @@
-
+
+ Add screen
{#each filteredScreens as screen (screen._id)}
store.actions.screens.select(screen._id)}
- color={RoleUtils.getRoleColour(screen.routing.roleId)}
+ rightAlignIcon
>
+
{/each}
{#if !filteredScreens?.length}
diff --git a/packages/builder/src/pages/builder/apps/index.svelte b/packages/builder/src/pages/builder/apps/index.svelte
index 03d39ddc45..1f5a761a42 100644
--- a/packages/builder/src/pages/builder/apps/index.svelte
+++ b/packages/builder/src/pages/builder/apps/index.svelte
@@ -13,7 +13,7 @@
notifications,
} from "@budibase/bbui"
import { onMount } from "svelte"
- import { apps, organisation, auth } from "stores/portal"
+ import { apps, organisation, auth, groups } from "stores/portal"
import { goto } from "@roxi/routify"
import { AppStatus } from "constants"
import { gradient } from "actions"
@@ -30,20 +30,43 @@
try {
await organisation.init()
await apps.load()
+ await groups.actions.init()
} catch (error) {
notifications.error("Error loading apps")
}
loaded = true
})
-
const publishedAppsOnly = app => app.status === AppStatus.DEPLOYED
+ $: userGroups = $groups.filter(group =>
+ group.users.find(user => user._id === $auth.user?._id)
+ )
+ let userApps = []
$: publishedApps = $apps.filter(publishedAppsOnly)
- $: userApps = $auth.user?.builder?.global
- ? publishedApps
- : publishedApps.filter(app =>
- Object.keys($auth.user?.roles).includes(app.prodId)
- )
+
+ $: {
+ if (!Object.keys($auth.user?.roles).length && $auth.user?.userGroups) {
+ userApps =
+ $auth.user?.builder?.global || $auth.user?.admin?.global
+ ? publishedApps
+ : publishedApps.filter(app => {
+ return userGroups.find(group => {
+ return Object.keys(group.roles)
+ .map(role => apps.extractAppId(role))
+ .includes(app.appId)
+ })
+ })
+ } else {
+ userApps =
+ $auth.user?.builder?.global || $auth.user?.admin?.global
+ ? publishedApps
+ : publishedApps.filter(app =>
+ Object.keys($auth.user?.roles)
+ .map(x => apps.extractAppId(x))
+ .includes(app.appId)
+ )
+ }
+ }
function getUrl(app) {
if (app.url) {
diff --git a/packages/builder/src/pages/builder/portal/_layout.svelte b/packages/builder/src/pages/builder/portal/_layout.svelte
index ae0362af72..21259c4d84 100644
--- a/packages/builder/src/pages/builder/portal/_layout.svelte
+++ b/packages/builder/src/pages/builder/portal/_layout.svelte
@@ -45,6 +45,7 @@
},
])
}
+
if (admin) {
menu = menu.concat([
{
@@ -65,6 +66,15 @@
},
])
+ if (isEnabled(FEATURE_FLAGS.USER_GROUPS)) {
+ let item = {
+ title: "User Groups",
+ href: "/builder/portal/manage/groups",
+ }
+
+ menu.splice(2, 0, item)
+ }
+
if (!$adminStore.cloud) {
menu = menu.concat([
{
diff --git a/packages/builder/src/pages/builder/portal/apps/_components/AcessFilter.svelte b/packages/builder/src/pages/builder/portal/apps/_components/AcessFilter.svelte
new file mode 100644
index 0000000000..d0662e7b41
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/apps/_components/AcessFilter.svelte
@@ -0,0 +1,43 @@
+
+
+
diff --git a/packages/builder/src/pages/builder/portal/apps/index.svelte b/packages/builder/src/pages/builder/portal/apps/index.svelte
index de5ad178cb..a089664d2e 100644
--- a/packages/builder/src/pages/builder/portal/apps/index.svelte
+++ b/packages/builder/src/pages/builder/portal/apps/index.svelte
@@ -20,12 +20,13 @@
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"
let sortBy = "name"
let template
@@ -39,6 +40,7 @@
let cloud = $admin.cloud
let creatingFromTemplate = false
let automationErrors
+ let accessFilterList = null
const resolveWelcomeMessage = (auth, apps) => {
const userWelcome = auth?.user?.firstName
@@ -56,8 +58,10 @@
: "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)
@@ -202,6 +206,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 +355,9 @@
{/if}
+ {#if $auth.groupsEnabled && $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..17c16c639b
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/manage/groups/[groupId].svelte
@@ -0,0 +1,260 @@
+
+
+{#if loaded}
+
+
+
$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}
+
+
+
+ {getRoleLabel(app.appId)}
+
+
+
+ {/each}
+ {:else}
+
+ {/if}
+
+
+{/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"}
+
+
+
+
+
+ $goto(`./${group._id}`)} size="S" cta
+ >Manage
+
+
+
+
+
+
+ deleteGroup(group)} icon="Delete"
+ >Delete
+ editGroup(group)} icon="Edit">Edit
+
+
+
+
+
+
+
+
+
+
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..ddd734dd69
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/manage/groups/index.svelte
@@ -0,0 +1,148 @@
+
+
+
+
+
+
User groups
+ {#if !$auth.groupsEnabled}
+
+
+
+ {/if}
+
+ Easily assign and manage your users access with User Groups
+
+
+
+ {$auth.groupsEnabled ? "Create user group" : "Upgrade Account"}
+
+ {#if !$auth.groupsEnabled}
+ {
+ window.open("https://budibase.com/pricing/", "_blank")
+ }}>View Plans
+ {/if}
+
+
+ {#if $auth.groupsEnabled && $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..8f7b24f1b6 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,138 @@
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,
+ Divider,
+ 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 { roles } from "stores/backend"
+ 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
+ let loaded = false
- 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],
- }
- })
-
- $: appList = allAppList.filter(app => !!app.role[0])
- $: noRoleAppList = allAppList
- .filter(app => !app.role[0])
- .map(app => {
- delete app.role
- return app
+ $: fetchUser(userId)
+ $: fullName = $userFetch?.data?.firstName
+ ? $userFetch?.data?.firstName + " " + $userFetch?.data?.lastName
+ : ""
+ $: nameLabel = getNameLabel($userFetch)
+ $: initials = getInitials(nameLabel)
+ $: allAppList = $apps
+ .filter(x => {
+ if ($userFetch.data?.roles) {
+ return Object.keys($userFetch.data.roles).find(y => {
+ return x.appId === apps.extractAppId(y)
+ })
+ }
})
-
- let selectedApp
+ .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
+ })
+ })
+ $: 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")
+ const getNameLabel = userFetch => {
+ const { firstName, lastName, email } = userFetch?.data || {}
+ if (!firstName && !lastName) {
+ return email || ""
}
+ let label
+ if (firstName) {
+ label = firstName
+ if (lastName) {
+ label += ` ${lastName}`
+ }
+ } else {
+ label = lastName
+ }
+ return label
}
- let toggleDisabled = false
+ const getInitials = nameLabel => {
+ if (!nameLabel) {
+ return "?"
+ }
+ return nameLabel
+ .split(" ")
+ .slice(0, 2)
+ .map(x => x[0])
+ .join("")
+ }
+ const getRoleLabel = roleId => {
+ const role = $roles.find(x => x._id === roleId)
+ return role?.name || "Custom role"
+ }
+
+ 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
+ }
async function updateUserFirstName(evt) {
try {
await users.save({ ...$userFetch?.data, firstName: evt.target.value })
@@ -84,6 +143,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,167 +159,215 @@
}
}
- 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: true } })
+ } 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 Promise.all([groups.actions.init(), apps.load(), roles.fetch()])
+ loaded = true
+ } catch (error) {
+ notifications.error("Error getting user groups")
+ }
+ })
-
-
+{#if loaded}
+
-
$goto("./")}
- quiet
- size="S"
- icon="BackAndroid"
- >
- Back to users
+ $goto("./")} 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.
-
-
-
-
- General
-
-
- Email
-
-
-
- Group(s)
-
-
-
- First name
-
-
-
- Last name
-
-
-
- {#if userId !== $auth.user._id}
-
- Development access
-
-
-
- Administration access
-
-
- {/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.
-
-
- Delete user
-
-
-
-
-
- Are you sure you want to delete {$userFetch?.data?.email}
-
-
-
-
-
+
+
+
+
+
+
+ {nameLabel}
+ {#if nameLabel !== $userFetch?.data?.email}
+ {$userFetch?.data?.email}
+ {/if}
+
+
+
+ {#if userId !== $auth.user._id}
+
+
+
+
+
+
+ Force password reset
+
+
+ Delete
+
+
+
+ {/if}
+
+
+
+ Details
+
+
+ Email
+
+
+
+ First name
+
+
+
+ Last name
+
+
+
+ {#if userId !== $auth.user._id}
+
+ Role
+
+
+ {/if}
+
+
+
+
+ {#if $auth.groupsEnabled}
+
+
+
+
User groups
+
+
+ Add to user group
+
+
+
+
+
+
+
+ {#if userGroups.length}
+ {#each userGroups as group}
+ $goto(`../groups/${group._id}`)}
+ >
+
+
+ {/each}
+ {:else}
+
+ {/if}
+
+
+ {/if}
+
+
+ Apps
+
+ {#if allAppList.length}
+ {#each allAppList as app}
+ $goto(`../../overview/${app.devId}`)}
+ >
+
+
+ {getRoleLabel(getHighestRole(app.roles))}
+
+
+
+ {/each}
+ {:else}
+
+ {/if}
+
+
+
+{/if}
+
+
+
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..165d94e0b5 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,134 @@
-
- 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.
-
-
+
+ Email address
+ {#each userData as input, index}
+
+
+ validateInput(input.email, index)}
+ />
+
+
+ removeInput(index)}
+ />
+
+
+ {/each}
+
+
-
-
- {#if basic}
-
+ {#if $auth.groupsEnabled}
+ option.name}
+ getOptionValue={option => option._id}
+ />
{/if}
-
-
-
- Development access
-
-
-
- Administration access
-
-
-
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..b334575669
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/manage/users/_components/GroupsTableRenderer.svelte
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+ {value?.length || 0}
+
+
+
+
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..1e7c579346
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/manage/users/_components/ImportUsersModal.svelte
@@ -0,0 +1,153 @@
+
+
+ createUsersFromCsv({ userEmails, usersRole, userGroups })}
+ disabled={!userEmails.length || !validEmails(userEmails) || !usersRole}
+>
+ Import your users email addresses from a CSV file
+
+
+
+
+ {#if files[0]}{files[0].name}{:else}Upload{/if}
+
+
+
+
+
+ {#if $auth.groupsEnabled}
+ 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..a4b65c4d62
--- /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}
+
-
+ {/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..02501f2de0
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/manage/users/_components/PasswordModal.svelte
@@ -0,0 +1,90 @@
+
+
+
+
+ All your new users can be accessed through the autogenerated passwords. Take
+ note of these passwords or download the CSV file.
+
+
+
+
+
+
+
+ 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..fe7acee6c4
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/manage/users/_components/RoleTableRenderer.svelte
@@ -0,0 +1,22 @@
+
+
+
+ {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..73cf5e26fa 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,203 @@
-
+
Users
-
- Each user is assigned to a group that contains apps and permissions. In
- this section, you can add users, or edit and delete an existing user.
-
+ Add users and control who gets access to your published apps
-
-
-
Users
-
- Import users
- Add user
-
+
+
+ Add users
+
+ Import users
+
+
+
+
+ {#if selectedRows.length > 0}
+
+ {/if}
-
- Search / filter
-
-
-
$goto(`./${detail._id}`)}
- {schema}
- data={$users.data}
- allowEditColumns={false}
- allowEditRows={false}
- allowSelectRows={false}
- customRenderers={[{ column: "group", component: TagsRenderer }]}
+
+ $goto(`./${detail._id}`)}
+ {schema}
+ bind:selectedRows
+ data={enrichedUsers}
+ allowEditColumns={false}
+ allowEditRows={false}
+ allowSelectRows={true}
+ showHeaderBorder={false}
+ {customRenderers}
+ />
+
- {
- pageInfo.reset()
- await fetchUsers()
- }}
- />
+
+
+
+
+
+ Your users should now recieve an email invite to get access to their
+ Budibase account
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/builder/src/pages/builder/portal/overview/[application]/index.svelte b/packages/builder/src/pages/builder/portal/overview/[application]/index.svelte
index 293f19ebe4..0de8fd95ce 100644
--- a/packages/builder/src/pages/builder/portal/overview/[application]/index.svelte
+++ b/packages/builder/src/pages/builder/portal/overview/[application]/index.svelte
@@ -19,6 +19,7 @@
} from "@budibase/bbui"
import OverviewTab from "../_components/OverviewTab.svelte"
import SettingsTab from "../_components/SettingsTab.svelte"
+ import AccessTab from "../_components/AccessTab.svelte"
import { API } from "api"
import { store } from "builderStore"
import { apps, auth } from "stores/portal"
@@ -65,7 +66,7 @@
selectedApp?.status === AppStatus.DEPLOYED && latestDeployments?.length > 0
$: appUrl = `${window.origin}/app${selectedApp?.url}`
- $: tabs = ["Overview", "Automation History", "Backups", "Settings"]
+ $: tabs = ["Overview", "Automation History", "Backups", "Settings", "Access"]
$: selectedTab = "Overview"
const backToAppList = () => {
@@ -139,9 +140,10 @@
notifications.success("App ID copied to clipboard.")
}
- const exportApp = app => {
- const id = isPublished ? app.prodId : app.devId
+ const exportApp = (app, opts = { published: false }) => {
const appName = encodeURIComponent(app.name)
+ const id = opts?.published ? app.prodId : app.devId
+ // always export the development version
window.location = `/api/backups/export?appId=${id}&appname=${appName}`
}
@@ -266,12 +268,21 @@
- exportApp(selectedApp)} icon="Download">
- Export
+ exportApp(selectedApp, { published: false })}
+ icon="DownloadFromCloud"
+ >
+ Export latest
{#if isPublished}
+ exportApp(selectedApp, { published: true })}
+ icon="DownloadFromCloudOutline"
+ >
+ Export published
+
copyAppId(selectedApp)} icon="Copy">
- Copy App ID
+ Copy app ID
{/if}
{#if !isPublished}
@@ -299,6 +310,9 @@
on:unpublish={e => unpublishApp(e.detail)}
/>
+
+
+
{#if isPublished}
diff --git a/packages/builder/src/pages/builder/portal/overview/_components/AccessTab.svelte b/packages/builder/src/pages/builder/portal/overview/_components/AccessTab.svelte
new file mode 100644
index 0000000000..5e327a8743
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/overview/_components/AccessTab.svelte
@@ -0,0 +1,276 @@
+
+
+
+
+ {#if appGroups.length || appUsers.length}
+
+
Access
+
+
+ Assign users to your app and define their access here
+ Assign users
+
+
+ {#if $auth.groupsEnabled && 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
+
+
+ assignmentModal.show()} cta icon="UserArrow"
+ >Assign Users
+
+
+
+ {/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..e3b2245679
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/overview/_components/AssignmentModal.svelte
@@ -0,0 +1,158 @@
+
+
+ addData(appData)}
+ showCloseIcon={false}
+ disabled={!valid}
+>
+ {#if appData?.length}
+
+ {#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)}
+ />
+
+
+ removeItem(index)}
+ />
+
+
+ {/each}
+
+ {/if}
+
+
+
+
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..19402c213e 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 +141,38 @@
{/if}
+ {
+ navigateTab("Access")
+ }}
+ dataCy={"access"}
+ >
+
+ {#if $users?.data?.length}
+
+
+ {#each $users?.data as user}
+
+ {/each}
+
+
+
+ {userCount}
+ {userCount > 1 ? `users have` : `user has`} access to this app
+
+
+ {:else}
+
+ No users
+
+ No users have been assigned to this app
+
+
+ {/if}
+
+
{#if false}
@@ -186,6 +227,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.
+
+ Update app
+
{:else}
-
+
The app is currently using version
{$store.version} . You're running the latest!
-
+
+
+
+ Revert app
+
+
{/if}
-
- Updates can contain new features, performance improvements and bug
- fixes.
-
-
- Update app
-