Merge pull request #7826 from Budibase/develop

Develop -> Master
This commit is contained in:
Martin McKeaveney 2022-09-20 13:16:44 +01:00 committed by GitHub
commit d72358e0a1
338 changed files with 14369 additions and 5373 deletions

24
.github/ISSUE_TEMPLATE/epic.md vendored Normal file
View File

@ -0,0 +1,24 @@
---
name: Epic
about: Plan a new project
title: ''
labels: epic
assignees: ''
---
## Description
Brief summary of what this Epic is, whether it's a larger project, goal, or user story. Describe the job to be done, which persona this Epic is mainly for, or if more multiple, break it down by user and job story.
## Spec
Link to confluence spec
## Teams and Stakeholders
Describe who needs to be kept up-to-date about this Epic, included in discussions, or updated along the way. Stakeholders can be both in Product/Engineering, as well as other teams like Customer Success who might want to keep customers updated on the Epic project.
## Workflow
- [ ] Spec Created and pasted above
- [ ] Product Review
- [ ] Designs created
- [ ] Individual Tasks created and assigned to Epic

View File

@ -59,3 +59,9 @@ jobs:
with: with:
install: false install: false
command: yarn test:e2e:ci command: yarn test:e2e:ci
- name: QA Core Integration Tests
run: |
cd qa-core
yarn
yarn api:test:ci

5
.gitignore vendored
View File

@ -63,6 +63,7 @@ typings/
# dotenv environment variables file # dotenv environment variables file
.env .env
!qa-core/.env
!hosting/.env !hosting/.env
hosting/.generated-nginx.dev.conf hosting/.generated-nginx.dev.conf
hosting/proxy/.generated-nginx.prod.conf hosting/proxy/.generated-nginx.prod.conf
@ -102,4 +103,6 @@ packages/builder/cypress/reports
stats.html stats.html
# TypeScript cache # TypeScript cache
*.tsbuildinfo *.tsbuildinfo
budibase-component
budibase-datasource

View File

@ -8,4 +8,4 @@ packages/server/client
packages/server/src/definitions/openapi.ts packages/server/src/definitions/openapi.ts
packages/builder/.routify packages/builder/.routify
packages/builder/cypress/support/queryLevelTransformerFunction.js packages/builder/cypress/support/queryLevelTransformerFunction.js
packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js

View File

@ -65,7 +65,7 @@ Budibase is open-source - licensed as GPL v3. This should fill you with confiden
<br /><br /> <br /><br />
### Load data or start from scratch ### Load data or start from scratch
Budibase pulls in data from multiple sources, including MongoDB, CouchDB, PostgreSQL, MySQL, Airtable, S3, DynamoDB, 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). Budibase pulls in data from multiple sources, including MongoDB, CouchDB, PostgreSQL, MySQL, Airtable, S3, DynamoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no datasources. [Request new datasources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
<p align="center"> <p align="center">
<img alt="Budibase data" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970242/Out%20of%20beta%20launch/data_n1tlhf.png"> <img alt="Budibase data" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970242/Out%20of%20beta%20launch/data_n1tlhf.png">

View File

@ -124,11 +124,15 @@ spec:
value: {{ .Values.globals.tenantFeatureFlags | quote }} value: {{ .Values.globals.tenantFeatureFlags | quote }}
{{ if .Values.globals.bbAdminUserEmail }} {{ if .Values.globals.bbAdminUserEmail }}
- name: BB_ADMIN_USER_EMAIL - name: BB_ADMIN_USER_EMAIL
value: { { .Values.globals.bbAdminUserEmail | quote } } value: {{ .Values.globals.bbAdminUserEmail | quote }}
{{ end }} {{ end }}
{{ if .Values.globals.bbAdminUserPassword }} {{ if .Values.globals.bbAdminUserPassword }}
- name: BB_ADMIN_USER_PASSWORD - name: BB_ADMIN_USER_PASSWORD
value: { { .Values.globals.bbAdminUserPassword | quote } } value: {{ .Values.globals.bbAdminUserPassword | quote }}
{{ end }}
{{ if .Values.globals.pluginsDir }}
- name: PLUGINS_DIR
value: {{ .Values.globals.pluginsDir | quote }}
{{ end }} {{ end }}
{{ if .Values.services.apps.nodeDebug }} {{ if .Values.services.apps.nodeDebug }}
- name: NODE_DEBUG - name: NODE_DEBUG
@ -158,7 +162,10 @@ spec:
name: bbapps name: bbapps
ports: ports:
- containerPort: {{ .Values.services.apps.port }} - containerPort: {{ .Values.services.apps.port }}
resources: {} {{ with .Values.services.apps.resources }}
resources:
{{- toYaml . | nindent 10 }}
{{ end }}
{{- with .Values.affinity }} {{- with .Values.affinity }}
affinity: affinity:
{{- toYaml . | nindent 8 }} {{- toYaml . | nindent 8 }}

View File

@ -38,7 +38,10 @@ spec:
image: redgeoff/replicate-couchdb-cluster image: redgeoff/replicate-couchdb-cluster
imagePullPolicy: Always imagePullPolicy: Always
name: couchdb-backup name: couchdb-backup
resources: {} {{ with .Values.services.couchdb.backup.resources }}
resources:
{{- toYaml . | nindent 10 }}
{{ end }}
{{- with .Values.affinity }} {{- with .Values.affinity }}
affinity: affinity:
{{- toYaml . | nindent 8 }} {{- toYaml . | nindent 8 }}

View File

@ -56,7 +56,10 @@ spec:
name: minio-service name: minio-service
ports: ports:
- containerPort: {{ .Values.services.objectStore.port }} - containerPort: {{ .Values.services.objectStore.port }}
resources: {} {{ with .Values.services.objectStore.resources }}
resources:
{{- toYaml . | nindent 10 }}
{{ end }}
volumeMounts: volumeMounts:
- mountPath: /data - mountPath: /data
name: minio-data name: minio-data

View File

@ -30,7 +30,10 @@ spec:
name: proxy-service name: proxy-service
ports: ports:
- containerPort: {{ .Values.services.proxy.port }} - containerPort: {{ .Values.services.proxy.port }}
resources: {} {{ with .Values.services.proxy.resources }}
resources:
{{- toYaml . | nindent 10 }}
{{ end }}
volumeMounts: volumeMounts:
{{- with .Values.affinity }} {{- with .Values.affinity }}
affinity: affinity:

View File

@ -35,7 +35,10 @@ spec:
name: redis-service name: redis-service
ports: ports:
- containerPort: {{ .Values.services.redis.port }} - containerPort: {{ .Values.services.redis.port }}
resources: {} {{ with .Values.services.redis.resources }}
resources:
{{- toYaml . | nindent 10 }}
{{ end }}
volumeMounts: volumeMounts:
- mountPath: /data - mountPath: /data
name: redis-data name: redis-data

View File

@ -151,7 +151,10 @@ spec:
name: bbworker name: bbworker
ports: ports:
- containerPort: {{ .Values.services.worker.port }} - containerPort: {{ .Values.services.worker.port }}
resources: {} {{ with .Values.services.worker.resources }}
resources:
{{- toYaml . | nindent 10 }}
{{ end }}
{{- with .Values.affinity }} {{- with .Values.affinity }}
affinity: affinity:
{{- toYaml . | nindent 8 }} {{- toYaml . | nindent 8 }}

View File

@ -60,19 +60,6 @@ ingress:
port: port:
number: 10000 number: 10000
resources:
{}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
autoscaling: autoscaling:
enabled: false enabled: false
minReplicas: 1 minReplicas: 1
@ -125,16 +112,19 @@ services:
proxy: proxy:
port: 10000 port: 10000
replicaCount: 1 replicaCount: 1
resources: {}
apps: apps:
port: 4002 port: 4002
replicaCount: 1 replicaCount: 1
logLevel: info logLevel: info
resources: {}
# nodeDebug: "" # set the value of NODE_DEBUG # nodeDebug: "" # set the value of NODE_DEBUG
worker: worker:
port: 4003 port: 4003
replicaCount: 1 replicaCount: 1
resources: {}
couchdb: couchdb:
enabled: true enabled: true
@ -148,6 +138,7 @@ services:
target: "" target: ""
# backup interval in seconds # backup interval in seconds
interval: "" interval: ""
resources: {}
redis: redis:
enabled: true # disable if using external redis enabled: true # disable if using external redis
@ -161,6 +152,7 @@ services:
## If undefined (the default) or set to null, no storageClassName spec is ## If undefined (the default) or set to null, no storageClassName spec is
## set, choosing the default provisioner. ## set, choosing the default provisioner.
storageClass: "" storageClass: ""
resources: {}
objectStore: objectStore:
minio: true minio: true
@ -177,6 +169,7 @@ services:
## If undefined (the default) or set to null, no storageClassName spec is ## If undefined (the default) or set to null, no storageClassName spec is
## set, choosing the default provisioner. ## set, choosing the default provisioner.
storageClass: "" storageClass: ""
resources: {}
# Override values in couchDB subchart # Override values in couchDB subchart
couchdb: couchdb:

View File

@ -348,7 +348,7 @@ export interface paths {
} }
} }
responses: { responses: {
/** Returns the created table, including the ID which has been generated for it. This can be internal or external data sources. */ /** Returns the created table, including the ID which has been generated for it. This can be internal or external datasources. */
200: { 200: {
content: { content: {
"application/json": components["schemas"]["tableOutput"] "application/json": components["schemas"]["tableOutput"]
@ -959,7 +959,7 @@ export interface components {
query: { query: {
/** @description The ID of the query. */ /** @description The ID of the query. */
_id: string _id: string
/** @description The ID of the data source the query belongs to. */ /** @description The ID of the datasource the query belongs to. */
datasourceId?: string datasourceId?: string
/** @description The bindings which are required to perform this query. */ /** @description The bindings which are required to perform this query. */
parameters?: string[] parameters?: string[]
@ -983,7 +983,7 @@ export interface components {
data: { data: {
/** @description The ID of the query. */ /** @description The ID of the query. */
_id: string _id: string
/** @description The ID of the data source the query belongs to. */ /** @description The ID of the datasource the query belongs to. */
datasourceId?: string datasourceId?: string
/** @description The bindings which are required to perform this query. */ /** @description The bindings which are required to perform this query. */
parameters?: string[] parameters?: string[]

View File

@ -11,8 +11,8 @@
"dependencies": { "dependencies": {
"bulma": "^0.9.3", "bulma": "^0.9.3",
"next": "12.1.0", "next": "12.1.0",
"node-fetch": "^3.2.2", "node-fetch": "^3.2.10",
"node-sass": "^7.0.1", "sass": "^1.52.3",
"react": "17.0.2", "react": "17.0.2",
"react-dom": "17.0.2", "react-dom": "17.0.2",
"react-notifications-component": "^3.4.1" "react-notifications-component": "^3.4.1"
@ -24,4 +24,4 @@
"eslint-config-next": "12.1.0", "eslint-config-next": "12.1.0",
"typescript": "4.6.2" "typescript": "4.6.2"
} }
} }

View File

@ -2020,10 +2020,10 @@ node-domexception@^1.0.0:
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
node-fetch@^3.2.2: node-fetch@^3.2.10:
version "3.2.2" version "3.2.10"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.2.tgz#16d33fbe32ca7c6ca1ca8ba5dfea1dd885c59f04" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.10.tgz#e8347f94b54ae18b57c9c049ef641cef398a85c8"
integrity sha512-Cwhq1JFIoon15wcIkFzubVNFE5GvXGV82pKf4knXXjvGmn7RJKcypeuqcVNZMGDZsAFWyIRya/anwAJr7TWJ7w== integrity sha512-MhuzNwdURnZ1Cp4XTazr69K0BTizsBroX7Zx3UgDSVcZYKF/6p0CBe4EUb/hLqmzVhl0UpYfgRljQ4yxE+iCxA==
dependencies: dependencies:
data-uri-to-buffer "^4.0.0" data-uri-to-buffer "^4.0.0"
fetch-blob "^3.1.4" fetch-blob "^3.1.4"

View File

@ -22,4 +22,7 @@ BUDIBASE_ENVIRONMENT=PRODUCTION
# An admin user can be automatically created initially if these are set # An admin user can be automatically created initially if these are set
BB_ADMIN_USER_EMAIL= BB_ADMIN_USER_EMAIL=
BB_ADMIN_USER_PASSWORD= BB_ADMIN_USER_PASSWORD=
# A path that is watched for plugin bundles. Any bundles found are imported automatically/
PLUGINS_DIR=

View File

@ -25,9 +25,12 @@ services:
REDIS_PASSWORD: ${REDIS_PASSWORD} REDIS_PASSWORD: ${REDIS_PASSWORD}
BB_ADMIN_USER_EMAIL: ${BB_ADMIN_USER_EMAIL} BB_ADMIN_USER_EMAIL: ${BB_ADMIN_USER_EMAIL}
BB_ADMIN_USER_PASSWORD: ${BB_ADMIN_USER_PASSWORD} BB_ADMIN_USER_PASSWORD: ${BB_ADMIN_USER_PASSWORD}
PLUGINS_DIR: ${PLUGINS_DIR}
depends_on: depends_on:
- worker-service - worker-service
- redis-service - redis-service
# volumes:
# - /some/path/to/plugins:/plugins
worker-service: worker-service:
restart: unless-stopped restart: unless-stopped

View File

@ -22,4 +22,7 @@ BUDIBASE_ENVIRONMENT=PRODUCTION
# An admin user can be automatically created initially if these are set # An admin user can be automatically created initially if these are set
BB_ADMIN_USER_EMAIL= BB_ADMIN_USER_EMAIL=
BB_ADMIN_USER_PASSWORD= BB_ADMIN_USER_PASSWORD=
# A path that is watched for plugin bundles. Any bundles found are imported automatically/
PLUGINS_DIR=

View File

@ -80,6 +80,20 @@ http {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
} }
location /vite/ {
proxy_pass http://{{ address }}:3000;
rewrite ^/vite(.*)$ /$1 break;
}
location /socket/ {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_pass http://{{ address }}:4001;
}
location / { location / {
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

View File

@ -158,6 +158,15 @@ http {
rewrite ^/db/(.*)$ /$1 break; rewrite ^/db/(.*)$ /$1 break;
} }
location /socket/ {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_pass http://$apps:4002;
}
location / { location / {
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

View File

@ -66,6 +66,15 @@ server {
rewrite ^/db/(.*)$ /$1 break; rewrite ^/db/(.*)$ /$1 break;
} }
location /socket/ {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_pass http://127.0.0.1:4001;
}
location / { location / {
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

View File

@ -1,5 +1,5 @@
{ {
"version": "1.3.21", "version": "1.3.22-alpha.4",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -13,6 +13,7 @@
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"kill-port": "^1.6.1", "kill-port": "^1.6.1",
"lerna": "3.14.1", "lerna": "3.14.1",
"madge": "^5.0.1",
"prettier": "^2.3.1", "prettier": "^2.3.1",
"prettier-plugin-svelte": "^2.3.0", "prettier-plugin-svelte": "^2.3.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
@ -25,6 +26,7 @@
"bootstrap": "lerna bootstrap && lerna link && ./scripts/link-dependencies.sh", "bootstrap": "lerna bootstrap && lerna link && ./scripts/link-dependencies.sh",
"build": "lerna run build", "build": "lerna run build",
"build:dev": "lerna run prebuild && tsc --build --watch --preserveWatchOutput", "build:dev": "lerna run prebuild && tsc --build --watch --preserveWatchOutput",
"deps:circular": "madge packages/server/dist/index.js packages/worker/src/index.ts packages/backend-core/dist/src/index.js packages/cli/src/index.js --circular",
"release": "lerna publish ${RELEASE_VERSION_TYPE:-patch} --yes --force-publish && yarn release:pro", "release": "lerna publish ${RELEASE_VERSION_TYPE:-patch} --yes --force-publish && yarn release:pro",
"release:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop --exact && 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": "bash scripts/pro/release.sh",
@ -45,8 +47,8 @@
"lint:eslint": "eslint packages", "lint:eslint": "eslint packages",
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\"", "lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\"",
"lint": "yarn run lint:eslint && yarn run lint:prettier", "lint": "yarn run lint:eslint && yarn run lint:prettier",
"lint:fix:eslint": "eslint --fix packages", "lint:fix:eslint": "eslint --fix packages qa-core",
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\"", "lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --write \"qa-core/**/*.{js,ts,svelte}\"",
"lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint", "lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint",
"test:e2e": "lerna run cy:test --stream", "test:e2e": "lerna run cy:test --stream",
"test:e2e:ci": "lerna run cy:ci --stream", "test:e2e:ci": "lerna run cy:ci --stream",

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/backend-core", "name": "@budibase/backend-core",
"version": "1.3.21", "version": "1.3.22-alpha.4",
"description": "Budibase backend core libraries used in server and worker", "description": "Budibase backend core libraries used in server and worker",
"main": "dist/src/index.js", "main": "dist/src/index.js",
"types": "dist/src/index.d.ts", "types": "dist/src/index.d.ts",
@ -20,11 +20,12 @@
"test:watch": "jest --watchAll" "test:watch": "jest --watchAll"
}, },
"dependencies": { "dependencies": {
"@budibase/types": "^1.3.21", "@budibase/types": "1.3.22-alpha.4",
"@shopify/jest-koa-mocks": "5.0.1", "@shopify/jest-koa-mocks": "5.0.1",
"@techpass/passport-openidconnect": "0.3.2", "@techpass/passport-openidconnect": "0.3.2",
"aws-sdk": "2.1030.0", "aws-sdk": "2.1030.0",
"bcrypt": "5.0.1", "bcrypt": "5.0.1",
"bcryptjs": "2.4.3",
"dotenv": "16.0.1", "dotenv": "16.0.1",
"emitter-listener": "1.1.2", "emitter-listener": "1.1.2",
"ioredis": "4.28.0", "ioredis": "4.28.0",

View File

@ -0,0 +1,3 @@
module.exports = {
...require("./src/plugin"),
}

View File

@ -7,6 +7,7 @@ exports.Cookies = {
CurrentApp: "budibase:currentapp", CurrentApp: "budibase:currentapp",
Auth: "budibase:auth", Auth: "budibase:auth",
Init: "budibase:init", Init: "budibase:init",
ACCOUNT_RETURN_URL: "budibase:account:returnurl",
DatasourceAuth: "budibase:datasourceauth", DatasourceAuth: "budibase:datasourceauth",
OIDC_CONFIG: "budibase:oidc:config", OIDC_CONFIG: "budibase:oidc:config",
} }

View File

@ -2,7 +2,7 @@ import env from "../environment"
import { SEPARATOR, DocumentType } from "../db/constants" import { SEPARATOR, DocumentType } from "../db/constants"
import cls from "./FunctionContext" import cls from "./FunctionContext"
import { dangerousGetDB, closeDB } from "../db" import { dangerousGetDB, closeDB } from "../db"
import { baseGlobalDBName } from "../tenancy/utils" import { baseGlobalDBName } from "../db/tenancy"
import { IdentityContext } from "@budibase/types" import { IdentityContext } from "@budibase/types"
import { DEFAULT_TENANT_ID as _DEFAULT_TENANT_ID } from "../constants" import { DEFAULT_TENANT_ID as _DEFAULT_TENANT_ID } from "../constants"
import { ContextKey } from "./constants" import { ContextKey } from "./constants"

View File

@ -44,6 +44,7 @@ export enum DocumentType {
DEV_INFO = "devinfo", DEV_INFO = "devinfo",
AUTOMATION_LOG = "log_au", AUTOMATION_LOG = "log_au",
ACCOUNT_METADATA = "acc_metadata", ACCOUNT_METADATA = "acc_metadata",
PLUGIN = "plg",
} }
export const StaticDatabases = { export const StaticDatabases = {

View File

@ -1,5 +1,5 @@
import { DEFAULT_TENANT_ID } from "../constants" import { DEFAULT_TENANT_ID } from "../constants"
import { StaticDatabases, SEPARATOR } from "../db/constants" import { StaticDatabases, SEPARATOR } from "./constants"
import { getTenantId } from "../context" import { getTenantId } from "../context"
export const getGlobalDBName = (tenantId?: string) => { export const getGlobalDBName = (tenantId?: string) => {

View File

@ -3,7 +3,7 @@ import { DEFAULT_TENANT_ID, Configs } from "../constants"
import env from "../environment" import env from "../environment"
import { SEPARATOR, DocumentType, UNICODE_MAX, ViewName } from "./constants" import { SEPARATOR, DocumentType, UNICODE_MAX, ViewName } from "./constants"
import { getTenantId, getGlobalDB } from "../context" import { getTenantId, getGlobalDB } from "../context"
import { getGlobalDBName } from "../tenancy/utils" import { getGlobalDBName } from "./tenancy"
import fetch from "node-fetch" import fetch from "node-fetch"
import { doWithDB, allDbs } from "./index" import { doWithDB, allDbs } from "./index"
import { getCouchInfo } from "./pouch" import { getCouchInfo } from "./pouch"
@ -16,6 +16,7 @@ import * as events from "../events"
export * from "./constants" export * from "./constants"
export * from "./conversions" export * from "./conversions"
export { default as Replication } from "./Replication" export { default as Replication } from "./Replication"
export * from "./tenancy"
/** /**
* Generates a new app ID. * Generates a new app ID.
@ -367,6 +368,21 @@ export const generateDevInfoID = (userId: any) => {
return `${DocumentType.DEV_INFO}${SEPARATOR}${userId}` return `${DocumentType.DEV_INFO}${SEPARATOR}${userId}`
} }
/**
* Generates a new plugin ID - to be used in the global DB.
* @returns {string} The new plugin ID which a plugin metadata document can be stored under.
*/
export const generatePluginID = (name: string) => {
return `${DocumentType.PLUGIN}${SEPARATOR}${name}`
}
/**
* Gets parameters for retrieving automations, this is a utility function for the getDocParams function.
*/
export const getPluginParams = (pluginId?: string | null, otherProps = {}) => {
return getDocParams(DocumentType.PLUGIN, pluginId, otherProps)
}
/** /**
* Returns the most granular configuration document from the DB based on the type, workspace and userID passed. * Returns the most granular configuration document from the DB based on the type, workspace and userID passed.
* @param {Object} db - db instance to query * @param {Object} db - db instance to query

View File

@ -1,5 +1,6 @@
import { DocumentType, ViewName, DeprecatedViews, SEPARATOR } from "./utils" import { DocumentType, ViewName, DeprecatedViews, SEPARATOR } from "./utils"
import { getGlobalDB } from "../context" import { getGlobalDB } from "../context"
import PouchDB from "pouchdb"
import { StaticDatabases } from "./constants" import { StaticDatabases } from "./constants"
import { doWithDB } from "./" import { doWithDB } from "./"
@ -201,13 +202,13 @@ export const queryView = async <T>(
try { try {
let response = await db.query<T, T>(`database/${viewName}`, params) let response = await db.query<T, T>(`database/${viewName}`, params)
const rows = response.rows const rows = response.rows
const docs = rows.map((resp: any) => const docs = rows.map(row => (params.include_docs ? row.doc : row.value))
params.include_docs ? resp.doc : resp.value
)
// if arrayResponse has been requested, always return array regardless of length
if (opts?.arrayResponse) { if (opts?.arrayResponse) {
return docs return docs
} else { } else {
// return the single document if there is only one
return docs.length <= 1 ? docs[0] : docs return docs.length <= 1 ? docs[0] : docs
} }
} catch (err: any) { } catch (err: any) {

View File

@ -19,6 +19,7 @@ if (!LOADED && isDev() && !isTest()) {
const env = { const env = {
isTest, isTest,
isDev, isDev,
JS_BCRYPT: process.env.JS_BCRYPT,
JWT_SECRET: process.env.JWT_SECRET, JWT_SECRET: process.env.JWT_SECRET,
COUCH_DB_URL: process.env.COUCH_DB_URL || "http://localhost:4005", COUCH_DB_URL: process.env.COUCH_DB_URL || "http://localhost:4005",
COUCH_DB_USERNAME: process.env.COUCH_DB_USER, COUCH_DB_USERNAME: process.env.COUCH_DB_USER,
@ -36,7 +37,7 @@ const env = {
MULTI_TENANCY: process.env.MULTI_TENANCY, MULTI_TENANCY: process.env.MULTI_TENANCY,
ACCOUNT_PORTAL_URL: ACCOUNT_PORTAL_URL:
process.env.ACCOUNT_PORTAL_URL || "https://account.budibase.app", process.env.ACCOUNT_PORTAL_URL || "https://account.budibase.app",
ACCOUNT_PORTAL_API_KEY: process.env.ACCOUNT_PORTAL_API_KEY, ACCOUNT_PORTAL_API_KEY: process.env.ACCOUNT_PORTAL_API_KEY || "",
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL, DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED || ""), SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED || ""),
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN, COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
@ -50,6 +51,7 @@ const env = {
GLOBAL_BUCKET_NAME: process.env.GLOBAL_BUCKET_NAME || "global", GLOBAL_BUCKET_NAME: process.env.GLOBAL_BUCKET_NAME || "global",
GLOBAL_CLOUD_BUCKET_NAME: GLOBAL_CLOUD_BUCKET_NAME:
process.env.GLOBAL_CLOUD_BUCKET_NAME || "prod-budi-tenant-uploads", process.env.GLOBAL_CLOUD_BUCKET_NAME || "prod-budi-tenant-uploads",
PLUGIN_BUCKET_NAME: process.env.PLUGIN_BUCKET_NAME || "plugins",
USE_COUCH: process.env.USE_COUCH || true, USE_COUCH: process.env.USE_COUCH || true,
DISABLE_DEVELOPER_LICENSE: process.env.DISABLE_DEVELOPER_LICENSE, DISABLE_DEVELOPER_LICENSE: process.env.DISABLE_DEVELOPER_LICENSE,
DEFAULT_LICENSE: process.env.DEFAULT_LICENSE, DEFAULT_LICENSE: process.env.DEFAULT_LICENSE,

View File

@ -1,11 +0,0 @@
class BudibaseError extends Error {
constructor(message, code, type) {
super(message)
this.code = code
this.type = type
}
}
module.exports = {
BudibaseError,
}

View File

@ -0,0 +1,10 @@
export class BudibaseError extends Error {
code: string
type: string
constructor(message: string, code: string, type: string) {
super(message)
this.code = code
this.type = type
}
}

View File

@ -1,11 +0,0 @@
const { BudibaseError } = require("./base")
class GenericError extends BudibaseError {
constructor(message, code, type) {
super(message, code, type ? type : "generic")
}
}
module.exports = {
GenericError,
}

View File

@ -0,0 +1,7 @@
import { BudibaseError } from "./base"
export class GenericError extends BudibaseError {
constructor(message: string, code: string, type: string) {
super(message, code, type ? type : "generic")
}
}

View File

@ -1,12 +0,0 @@
const { GenericError } = require("./generic")
class HTTPError extends GenericError {
constructor(message, httpStatus, code = "http", type = "generic") {
super(message, code, type)
this.status = httpStatus
}
}
module.exports = {
HTTPError,
}

View File

@ -0,0 +1,15 @@
import { GenericError } from "./generic"
export class HTTPError extends GenericError {
status: number
constructor(
message: string,
httpStatus: number,
code = "http",
type = "generic"
) {
super(message, code, type)
this.status = httpStatus
}
}

View File

@ -1,5 +1,6 @@
const http = require("./http") import { HTTPError } from "./http"
const licensing = require("./licensing") import { UsageLimitError, FeatureDisabledError } from "./licensing"
import * as licensing from "./licensing"
const codes = { const codes = {
...licensing.codes, ...licensing.codes,
@ -11,7 +12,7 @@ const context = {
...licensing.context, ...licensing.context,
} }
const getPublicError = err => { const getPublicError = (err: any) => {
let error let error
if (err.code || err.type) { if (err.code || err.type) {
// add generic error information // add generic error information
@ -32,13 +33,15 @@ const getPublicError = err => {
return error return error
} }
module.exports = { const pkg = {
codes, codes,
types, types,
errors: { errors: {
UsageLimitError: licensing.UsageLimitError, UsageLimitError,
FeatureDisabledError: licensing.FeatureDisabledError, FeatureDisabledError,
HTTPError: http.HTTPError, HTTPError,
}, },
getPublicError, getPublicError,
} }
export = pkg

View File

@ -1,43 +0,0 @@
const { HTTPError } = require("./http")
const type = "license_error"
const codes = {
USAGE_LIMIT_EXCEEDED: "usage_limit_exceeded",
FEATURE_DISABLED: "feature_disabled",
}
const context = {
[codes.USAGE_LIMIT_EXCEEDED]: err => {
return {
limitName: err.limitName,
}
},
[codes.FEATURE_DISABLED]: err => {
return {
featureName: err.featureName,
}
},
}
class UsageLimitError extends HTTPError {
constructor(message, limitName) {
super(message, 400, codes.USAGE_LIMIT_EXCEEDED, type)
this.limitName = limitName
}
}
class FeatureDisabledError extends HTTPError {
constructor(message, featureName) {
super(message, 400, codes.FEATURE_DISABLED, type)
this.featureName = featureName
}
}
module.exports = {
type,
codes,
context,
UsageLimitError,
FeatureDisabledError,
}

View File

@ -0,0 +1,39 @@
import { HTTPError } from "./http"
export const type = "license_error"
export const codes = {
USAGE_LIMIT_EXCEEDED: "usage_limit_exceeded",
FEATURE_DISABLED: "feature_disabled",
}
export const context = {
[codes.USAGE_LIMIT_EXCEEDED]: (err: any) => {
return {
limitName: err.limitName,
}
},
[codes.FEATURE_DISABLED]: (err: any) => {
return {
featureName: err.featureName,
}
},
}
export class UsageLimitError extends HTTPError {
limitName: string
constructor(message: string, limitName: string) {
super(message, 400, codes.USAGE_LIMIT_EXCEEDED, type)
this.limitName = limitName
}
}
export class FeatureDisabledError extends HTTPError {
featureName: string
constructor(message: string, featureName: string) {
super(message, 400, codes.FEATURE_DISABLED, type)
this.featureName = featureName
}
}

View File

@ -5,8 +5,15 @@ import {
DatasourceCreatedEvent, DatasourceCreatedEvent,
DatasourceUpdatedEvent, DatasourceUpdatedEvent,
DatasourceDeletedEvent, DatasourceDeletedEvent,
SourceName,
} from "@budibase/types" } from "@budibase/types"
function isCustom(datasource: Datasource) {
const sources = Object.values(SourceName)
// if not in the base source list, then it must be custom
return !sources.includes(datasource.source)
}
export async function created( export async function created(
datasource: Datasource, datasource: Datasource,
timestamp?: string | number timestamp?: string | number
@ -14,6 +21,7 @@ export async function created(
const properties: DatasourceCreatedEvent = { const properties: DatasourceCreatedEvent = {
datasourceId: datasource._id as string, datasourceId: datasource._id as string,
source: datasource.source, source: datasource.source,
custom: isCustom(datasource),
} }
await publishEvent(Event.DATASOURCE_CREATED, properties, timestamp) await publishEvent(Event.DATASOURCE_CREATED, properties, timestamp)
} }
@ -22,6 +30,7 @@ export async function updated(datasource: Datasource) {
const properties: DatasourceUpdatedEvent = { const properties: DatasourceUpdatedEvent = {
datasourceId: datasource._id as string, datasourceId: datasource._id as string,
source: datasource.source, source: datasource.source,
custom: isCustom(datasource),
} }
await publishEvent(Event.DATASOURCE_UPDATED, properties) await publishEvent(Event.DATASOURCE_UPDATED, properties)
} }
@ -30,6 +39,7 @@ export async function deleted(datasource: Datasource) {
const properties: DatasourceDeletedEvent = { const properties: DatasourceDeletedEvent = {
datasourceId: datasource._id as string, datasourceId: datasource._id as string,
source: datasource.source, source: datasource.source,
custom: isCustom(datasource),
} }
await publishEvent(Event.DATASOURCE_DELETED, properties) await publishEvent(Event.DATASOURCE_DELETED, properties)
} }

View File

@ -18,3 +18,4 @@ export * as view from "./view"
export * as installation from "./installation" export * as installation from "./installation"
export * as backfill from "./backfill" export * as backfill from "./backfill"
export * as group from "./group" export * as group from "./group"
export * as plugin from "./plugin"

View File

@ -0,0 +1,41 @@
import { publishEvent } from "../events"
import {
Event,
Plugin,
PluginDeletedEvent,
PluginImportedEvent,
PluginInitEvent,
} from "@budibase/types"
export async function init(plugin: Plugin) {
const properties: PluginInitEvent = {
type: plugin.schema.type,
name: plugin.name,
description: plugin.description,
version: plugin.version,
}
await publishEvent(Event.PLUGIN_INIT, properties)
}
export async function imported(plugin: Plugin) {
const properties: PluginImportedEvent = {
pluginId: plugin._id as string,
type: plugin.schema.type,
source: plugin.source,
name: plugin.name,
description: plugin.description,
version: plugin.version,
}
await publishEvent(Event.PLUGIN_IMPORTED, properties)
}
export async function deleted(plugin: Plugin) {
const properties: PluginDeletedEvent = {
pluginId: plugin._id as string,
type: plugin.schema.type,
name: plugin.name,
description: plugin.description,
version: plugin.version,
}
await publishEvent(Event.PLUGIN_DELETED, properties)
}

View File

@ -31,20 +31,26 @@ const TENANT_FEATURE_FLAGS = getFeatureFlags()
exports.isEnabled = featureFlag => { exports.isEnabled = featureFlag => {
const tenantId = tenancy.getTenantId() const tenantId = tenancy.getTenantId()
const flags = exports.getTenantFeatureFlags(tenantId)
return ( return flags.includes(featureFlag)
TENANT_FEATURE_FLAGS &&
TENANT_FEATURE_FLAGS[tenantId] &&
TENANT_FEATURE_FLAGS[tenantId].includes(featureFlag)
)
} }
exports.getTenantFeatureFlags = tenantId => { exports.getTenantFeatureFlags = tenantId => {
if (TENANT_FEATURE_FLAGS && TENANT_FEATURE_FLAGS[tenantId]) { const flags = []
return TENANT_FEATURE_FLAGS[tenantId]
if (TENANT_FEATURE_FLAGS) {
const globalFlags = TENANT_FEATURE_FLAGS["*"]
const tenantFlags = TENANT_FEATURE_FLAGS[tenantId]
if (globalFlags) {
flags.push(...globalFlags)
}
if (tenantFlags) {
flags.push(...tenantFlags)
}
} }
return [] return flags
} }
exports.FeatureFlag = { exports.FeatureFlag = {

View File

@ -1,5 +1,5 @@
const bcrypt = require("bcrypt")
const env = require("./environment") const env = require("./environment")
const bcrypt = env.JS_BCRYPT ? require("bcryptjs") : require("bcrypt")
const { v4 } = require("uuid") const { v4 } = require("uuid")
const SALT_ROUNDS = env.SALT_ROUNDS || 10 const SALT_ROUNDS = env.SALT_ROUNDS || 10

View File

@ -1,5 +1,4 @@
import errors from "./errors" import errors from "./errors"
const errorClasses = errors.errors const errorClasses = errors.errors
import * as events from "./events" import * as events from "./events"
import * as migrations from "./migrations" import * as migrations from "./migrations"
@ -15,9 +14,10 @@ import deprovisioning from "./context/deprovision"
import auth from "./auth" import auth from "./auth"
import constants from "./constants" import constants from "./constants"
import * as dbConstants from "./db/constants" import * as dbConstants from "./db/constants"
import logging from "./logging" import * as logging from "./logging"
import pino from "./pino" import pino from "./pino"
import * as middleware from "./middleware" import * as middleware from "./middleware"
import plugins from "./plugin"
// mimic the outer package exports // mimic the outer package exports
import * as db from "./pkg/db" import * as db from "./pkg/db"
@ -56,6 +56,7 @@ const core = {
errors, errors,
logging, logging,
roles, roles,
plugins,
...pino, ...pino,
...errorClasses, ...errorClasses,
middleware, middleware,

View File

@ -106,6 +106,7 @@ export = (
user = await getUser(userId, session.tenantId) user = await getUser(userId, session.tenantId)
} }
user.csrfToken = session.csrfToken user.csrfToken = session.csrfToken
if (session?.lastAccessedAt < timeMinusOneMinute()) { if (session?.lastAccessedAt < timeMinusOneMinute()) {
// make sure we denote that the session is still in use // make sure we denote that the session is still in use
await updateSessionTTL(session) await updateSessionTTL(session)

View File

@ -17,14 +17,6 @@ export const DEFINITIONS: MigrationDefinition[] = [
type: MigrationType.APP, type: MigrationType.APP,
name: MigrationName.APP_URLS, name: MigrationName.APP_URLS,
}, },
{
type: MigrationType.GLOBAL,
name: MigrationName.DEVELOPER_QUOTA,
},
{
type: MigrationType.GLOBAL,
name: MigrationName.PUBLISHED_APP_QUOTA,
},
{ {
type: MigrationType.APP, type: MigrationType.APP,
name: MigrationName.EVENT_APP_BACKFILL, name: MigrationName.EVENT_APP_BACKFILL,

View File

@ -3,12 +3,8 @@ import { doWithDB } from "../db"
import { DocumentType, StaticDatabases } from "../db/constants" import { DocumentType, StaticDatabases } from "../db/constants"
import { getAllApps } from "../db/utils" import { getAllApps } from "../db/utils"
import environment from "../environment" import environment from "../environment"
import { import { doInTenant, getTenantIds, getTenantId } from "../tenancy"
doInTenant, import { getGlobalDBName } from "../db/tenancy"
getTenantIds,
getGlobalDBName,
getTenantId,
} from "../tenancy"
import * as context from "../context" import * as context from "../context"
import { DEFINITIONS } from "." import { DEFINITIONS } from "."
import { import {

View File

@ -57,7 +57,11 @@ function publicPolicy(bucketName: any) {
} }
} }
const PUBLIC_BUCKETS = [ObjectStoreBuckets.APPS, ObjectStoreBuckets.GLOBAL] const PUBLIC_BUCKETS = [
ObjectStoreBuckets.APPS,
ObjectStoreBuckets.GLOBAL,
ObjectStoreBuckets.PLUGINS,
]
/** /**
* Gets a connection to the object store using the S3 SDK. * Gets a connection to the object store using the S3 SDK.
@ -172,6 +176,14 @@ export const streamUpload = async (
const objectStore = ObjectStore(bucketName) const objectStore = ObjectStore(bucketName)
await makeSureBucketExists(objectStore, bucketName) await makeSureBucketExists(objectStore, bucketName)
// Set content type for certain known extensions
if (filename?.endsWith(".js")) {
extra = {
...extra,
ContentType: "application/javascript",
}
}
const params = { const params = {
Bucket: sanitizeBucket(bucketName), Bucket: sanitizeBucket(bucketName),
Key: sanitizeKey(filename), Key: sanitizeKey(filename),
@ -295,9 +307,13 @@ export const uploadDirectory = async (
return files return files
} }
exports.downloadTarballDirect = async (url: string, path: string) => { exports.downloadTarballDirect = async (
url: string,
path: string,
headers = {}
) => {
path = sanitizeKey(path) path = sanitizeKey(path)
const response = await fetch(url) const response = await fetch(url, { headers })
if (!response.ok) { if (!response.ok) {
throw new Error(`unexpected response ${response.statusText}`) throw new Error(`unexpected response ${response.statusText}`)
} }

View File

@ -8,6 +8,7 @@ exports.ObjectStoreBuckets = {
TEMPLATES: env.TEMPLATES_BUCKET_NAME, TEMPLATES: env.TEMPLATES_BUCKET_NAME,
GLOBAL: env.GLOBAL_BUCKET_NAME, GLOBAL: env.GLOBAL_BUCKET_NAME,
GLOBAL_CLOUD: env.GLOBAL_CLOUD_BUCKET_NAME, GLOBAL_CLOUD: env.GLOBAL_CLOUD_BUCKET_NAME,
PLUGINS: env.PLUGIN_BUCKET_NAME,
} }
exports.budibaseTempDir = function () { exports.budibaseTempDir = function () {

View File

@ -0,0 +1,7 @@
import * as utils from "./utils"
const pkg = {
...utils,
}
export = pkg

View File

@ -0,0 +1,94 @@
const {
DatasourceFieldType,
QueryType,
PluginType,
} = require("@budibase/types")
const joi = require("joi")
const DATASOURCE_TYPES = [
"Relational",
"Non-relational",
"Spreadsheet",
"Object store",
"Graph",
"API",
]
function runJoi(validator, schema) {
const { error } = validator.validate(schema)
if (error) {
throw error
}
}
function validateComponent(schema) {
const validator = joi.object({
type: joi.string().allow("component").required(),
metadata: joi.object().unknown(true).required(),
hash: joi.string().optional(),
version: joi.string().optional(),
schema: joi
.object({
name: joi.string().required(),
settings: joi.array().items(joi.object().unknown(true)).required(),
})
.unknown(true),
})
runJoi(validator, schema)
}
function validateDatasource(schema) {
const fieldValidator = joi.object({
type: joi
.string()
.allow(...Object.values(DatasourceFieldType))
.required(),
required: joi.boolean().required(),
default: joi.any(),
display: joi.string(),
})
const queryValidator = joi
.object({
type: joi.string().allow(...Object.values(QueryType)),
fields: joi.object().pattern(joi.string(), fieldValidator),
})
.required()
const validator = joi.object({
type: joi.string().allow("datasource").required(),
metadata: joi.object().unknown(true).required(),
hash: joi.string().optional(),
version: joi.string().optional(),
schema: joi.object({
docs: joi.string(),
friendlyName: joi.string().required(),
type: joi.string().allow(...DATASOURCE_TYPES),
description: joi.string().required(),
datasource: joi.object().pattern(joi.string(), fieldValidator).required(),
query: joi
.object({
create: queryValidator,
read: queryValidator,
update: queryValidator,
delete: queryValidator,
})
.unknown(true)
.required(),
}),
})
runJoi(validator, schema)
}
exports.validate = schema => {
switch (schema?.type) {
case PluginType.COMPONENT:
validateComponent(schema)
break
case PluginType.DATASOURCE:
validateDatasource(schema)
break
default:
throw new Error(`Unknown plugin type - check schema.json: ${schema.type}`)
}
}

View File

@ -2,28 +2,12 @@ const redis = require("../redis/init")
const { v4: uuidv4 } = require("uuid") const { v4: uuidv4 } = require("uuid")
const { logWarn } = require("../logging") const { logWarn } = require("../logging")
const env = require("../environment") const env = require("../environment")
import {
interface CreateSession { Session,
sessionId: string ScannedSession,
tenantId: string SessionKey,
csrfToken?: string CreateSession,
} } from "@budibase/types"
interface Session extends CreateSession {
userId: string
lastAccessedAt: string
createdAt: string
// make optional attributes required
csrfToken: string
}
interface SessionKey {
key: string
}
interface ScannedSession {
value: Session
}
// a week in seconds // a week in seconds
const EXPIRY_SECONDS = 86400 * 7 const EXPIRY_SECONDS = 86400 * 7

View File

@ -1,11 +1,9 @@
import * as context from "../context" import * as context from "../context"
import * as tenancy from "./tenancy" import * as tenancy from "./tenancy"
import * as utils from "./utils"
const pkg = { const pkg = {
...context, ...context,
...tenancy, ...tenancy,
...utils,
} }
export = pkg export = pkg

View File

@ -1,7 +1,7 @@
import { doWithDB } from "../db" import { doWithDB } from "../db"
import { queryPlatformView } from "../db/views" import { queryPlatformView } from "../db/views"
import { StaticDatabases, ViewName } from "../db/constants" import { StaticDatabases, ViewName } from "../db/constants"
import { getGlobalDBName } from "./utils" import { getGlobalDBName } from "../db/tenancy"
import { import {
getTenantId, getTenantId,
DEFAULT_TENANT_ID, DEFAULT_TENANT_ID,
@ -9,7 +9,7 @@ import {
getTenantIDFromAppID, getTenantIDFromAppID,
} from "../context" } from "../context"
import env from "../environment" import env from "../environment"
import { PlatformUser, PlatformUserByEmail } from "@budibase/types" import { PlatformUser } from "@budibase/types"
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants
const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name

View File

@ -1,29 +1,39 @@
const { import {
ViewName, ViewName,
getUsersByAppParams, getUsersByAppParams,
getProdAppID, getProdAppID,
generateAppUserID, generateAppUserID,
} = require("./db/utils") } from "./db/utils"
const { queryGlobalView } = require("./db/views") import { queryGlobalView } from "./db/views"
const { UNICODE_MAX } = require("./db/constants") import { UNICODE_MAX } from "./db/constants"
import { User } from "@budibase/types"
/** /**
* Given an email address this will use a view to search through * Given an email address this will use a view to search through
* all the users to find one with this email address. * all the users to find one with this email address.
* @param {string} email the email to lookup the user by. * @param {string} email the email to lookup the user by.
*/ */
exports.getGlobalUserByEmail = async email => { export const getGlobalUserByEmail = async (
email: String
): Promise<User | undefined> => {
if (email == null) { if (email == null) {
throw "Must supply an email address to view" throw "Must supply an email address to view"
} }
return await queryGlobalView(ViewName.USER_BY_EMAIL, { const response = await queryGlobalView<User>(ViewName.USER_BY_EMAIL, {
key: email.toLowerCase(), key: email.toLowerCase(),
include_docs: true, include_docs: true,
}) })
if (Array.isArray(response)) {
// shouldn't be able to happen, but need to handle just in case
throw new Error(`Multiple users found with email address: ${email}`)
}
return response
} }
exports.searchGlobalUsersByApp = async (appId, opts) => { export const searchGlobalUsersByApp = async (appId: any, opts: any) => {
if (typeof appId !== "string") { if (typeof appId !== "string") {
throw new Error("Must provide a string based app ID") throw new Error("Must provide a string based app ID")
} }
@ -38,24 +48,24 @@ exports.searchGlobalUsersByApp = async (appId, opts) => {
return Array.isArray(response) ? response : [response] return Array.isArray(response) ? response : [response]
} }
exports.getGlobalUserByAppPage = (appId, user) => { export const getGlobalUserByAppPage = (appId: string, user: User) => {
if (!user) { if (!user) {
return return
} }
return generateAppUserID(getProdAppID(appId), user._id) return generateAppUserID(getProdAppID(appId), user._id!)
} }
/** /**
* Performs a starts with search on the global email view. * Performs a starts with search on the global email view.
*/ */
exports.searchGlobalUsersByEmail = async (email, opts) => { export const searchGlobalUsersByEmail = async (email: string, opts: any) => {
if (typeof email !== "string") { if (typeof email !== "string") {
throw new Error("Must provide a string to search by") throw new Error("Must provide a string to search by")
} }
const lcEmail = email.toLowerCase() const lcEmail = email.toLowerCase()
// handle if passing up startkey for pagination // handle if passing up startkey for pagination
const startkey = opts && opts.startkey ? opts.startkey : lcEmail const startkey = opts && opts.startkey ? opts.startkey : lcEmail
let response = await queryGlobalView(ViewName.USER_BY_EMAIL, { let response = await queryGlobalView<User>(ViewName.USER_BY_EMAIL, {
...opts, ...opts,
startkey, startkey,
endkey: `${lcEmail}${UNICODE_MAX}`, endkey: `${lcEmail}${UNICODE_MAX}`,

View File

@ -42,6 +42,18 @@ async function resolveAppUrl(ctx) {
return app && app.appId ? app.appId : undefined return app && app.appId ? app.appId : undefined
} }
exports.isServingApp = ctx => {
// dev app
if (ctx.path.startsWith(`/${APP_PREFIX}`)) {
return true
}
// prod app
if (ctx.path.startsWith(PROD_APP_PREFIX)) {
return true
}
return false
}
/** /**
* Given a request tries to find the appId, which can be located in various places * Given a request tries to find the appId, which can be located in various places
* @param {object} ctx The main request body to look through. * @param {object} ctx The main request body to look through.

View File

@ -1377,6 +1377,11 @@ bcrypt@5.0.1:
"@mapbox/node-pre-gyp" "^1.0.0" "@mapbox/node-pre-gyp" "^1.0.0"
node-addon-api "^3.1.0" node-addon-api "^3.1.0"
bcryptjs@2.4.3:
version "2.4.3"
resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb"
integrity sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==
binary-extensions@^2.0.0: binary-extensions@^2.0.0:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/bbui", "name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.", "description": "A UI solution used in the different Budibase projects.",
"version": "1.3.21", "version": "1.3.22-alpha.4",
"license": "MPL-2.0", "license": "MPL-2.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"module": "dist/bbui.es.js", "module": "dist/bbui.es.js",
@ -38,7 +38,7 @@
], ],
"dependencies": { "dependencies": {
"@adobe/spectrum-css-workflow-icons": "^1.2.1", "@adobe/spectrum-css-workflow-icons": "^1.2.1",
"@budibase/string-templates": "^1.3.21", "@budibase/string-templates": "1.3.22-alpha.4",
"@spectrum-css/actionbutton": "^1.0.1", "@spectrum-css/actionbutton": "^1.0.1",
"@spectrum-css/actiongroup": "^1.0.1", "@spectrum-css/actiongroup": "^1.0.1",
"@spectrum-css/avatar": "^3.0.2", "@spectrum-css/avatar": "^3.0.2",

View File

@ -1,4 +1,4 @@
export default function positionDropdown(element, { anchor, align }) { export default function positionDropdown(element, { anchor, align, maxWidth }) {
let positionSide = "top" let positionSide = "top"
let maxHeight = 0 let maxHeight = 0
let dimensions = getDimensions(anchor) let dimensions = getDimensions(anchor)
@ -34,13 +34,24 @@ export default function positionDropdown(element, { anchor, align }) {
} }
function calcLeftPosition() { function calcLeftPosition() {
return align === "right" let left
? dimensions.left + dimensions.width - dimensions.containerWidth
: dimensions.left if (align == "right") {
left = dimensions.left + dimensions.width - dimensions.containerWidth
} else if (align == "right-side") {
left = dimensions.left + dimensions.width
} else {
left = dimensions.left
}
return left
} }
element.style.position = "absolute" element.style.position = "absolute"
element.style.zIndex = "9999" element.style.zIndex = "9999"
if (maxWidth) {
element.style.maxWidth = `${maxWidth}px`
}
element.style.minWidth = `${dimensions.width}px` element.style.minWidth = `${dimensions.width}px`
element.style.maxHeight = `${maxHeight.toFixed(0)}px` element.style.maxHeight = `${maxHeight.toFixed(0)}px`
element.style.transformOrigin = `center ${positionSide}` element.style.transformOrigin = `center ${positionSide}`
@ -54,10 +65,8 @@ export default function positionDropdown(element, { anchor, align }) {
element.style.left = `${calcLeftPosition(dimensions).toFixed(0)}px` element.style.left = `${calcLeftPosition(dimensions).toFixed(0)}px`
}) })
}) })
resizeObserver.observe(anchor) resizeObserver.observe(anchor)
resizeObserver.observe(element) resizeObserver.observe(element)
return { return {
destroy() { destroy() {
resizeObserver.disconnect() resizeObserver.disconnect()

View File

@ -4,22 +4,32 @@
import { banner } from "../Stores/banner" import { banner } from "../Stores/banner"
import Banner from "./Banner.svelte" import Banner from "./Banner.svelte"
import { fly } from "svelte/transition" import { fly } from "svelte/transition"
import TooltipWrapper from "../Tooltip/TooltipWrapper.svelte"
</script> </script>
<Portal target=".banner-container"> <Portal target=".banner-container">
<div class="banner"> <div class="banner">
{#if $banner.message} {#each $banner.messages as message}
<div transition:fly={{ y: -30 }}> <div transition:fly={{ y: -30 }}>
<Banner <Banner
type={$banner.type} type={message.type}
extraButtonText={$banner.extraButtonText} extraButtonText={message.extraButtonText}
extraButtonAction={$banner.extraButtonAction} extraButtonAction={message.extraButtonAction}
on:change={$banner.onChange} on:change={() => {
if (message.onChange) {
message.onChange()
}
}}
showCloseButton={typeof message.showCloseButton === "boolean"
? message.showCloseButton
: true}
> >
{$banner.message} <TooltipWrapper tooltip={message.tooltip} disabled={false}>
{message.message}
</TooltipWrapper>
</Banner> </Banner>
</div> </div>
{/if} {/each}
</div> </div>
</Portal> </Portal>

View File

@ -6,6 +6,7 @@
export let header = "" export let header = ""
export let message = "" export let message = ""
export let onConfirm = undefined export let onConfirm = undefined
export let buttonText = ""
$: icon = selectIcon(type) $: icon = selectIcon(type)
// if newlines used, convert them to different elements // if newlines used, convert them to different elements
@ -39,13 +40,16 @@
<div class="spectrum-InLineAlert-content">{splitMsg}</div> <div class="spectrum-InLineAlert-content">{splitMsg}</div>
{/each} {/each}
{#if onConfirm} {#if onConfirm}
<div class="spectrum-InLineAlert-footer"> <div class="spectrum-InLineAlert-footer button">
<Button secondary on:click={onConfirm}>OK</Button> <Button secondary on:click={onConfirm}>{buttonText || "OK"}</Button>
</div> </div>
{/if} {/if}
</div> </div>
<style> <style>
.button {
margin-top: 10px;
}
.spectrum-InLineAlert { .spectrum-InLineAlert {
--spectrum-semantic-negative-border-color: #e34850; --spectrum-semantic-negative-border-color: #e34850;
--spectrum-semantic-positive-border-color: #2d9d78; --spectrum-semantic-positive-border-color: #2d9d78;

View File

@ -4,10 +4,15 @@
export let size = "M" export let size = "M"
export let tooltip = "" export let tooltip = ""
export let muted
</script> </script>
<TooltipWrapper {tooltip} {size}> <TooltipWrapper {tooltip} {size}>
<label for="" class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`}> <label
class:muted
for=""
class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`}
>
<slot /> <slot />
</label> </label>
</TooltipWrapper> </TooltipWrapper>
@ -17,4 +22,8 @@
padding: 0; padding: 0;
white-space: nowrap; white-space: nowrap;
} }
.muted {
opacity: 0.5;
}
</style> </style>

View File

@ -24,7 +24,6 @@
export let secondaryAction = undefined export let secondaryAction = undefined
export let secondaryButtonWarning = false export let secondaryButtonWarning = false
export let dataCy = null export let dataCy = null
const { hide, cancel } = getContext(Context.Modal) const { hide, cancel } = getContext(Context.Modal)
let loading = false let loading = false
$: confirmDisabled = disabled || loading $: confirmDisabled = disabled || loading
@ -88,12 +87,11 @@
<section class="spectrum-Dialog-content content-grid"> <section class="spectrum-Dialog-content content-grid">
<slot /> <slot />
</section> </section>
{#if showCancelButton || showConfirmButton} {#if showCancelButton || showConfirmButton || $$slots.footer}
<div <div
class="spectrum-ButtonGroup spectrum-Dialog-buttonGroup spectrum-Dialog-buttonGroup--noFooter" class="spectrum-ButtonGroup spectrum-Dialog-buttonGroup spectrum-Dialog-buttonGroup--noFooter"
> >
<slot name="footer" /> <slot name="footer" />
{#if showSecondaryButton && secondaryButtonText && secondaryAction} {#if showSecondaryButton && secondaryButtonText && secondaryAction}
<div class="secondary-action"> <div class="secondary-action">
<Button <Button

View File

@ -11,6 +11,7 @@
export let align = "right" export let align = "right"
export let portalTarget export let portalTarget
export let dataCy export let dataCy
export let maxWidth
export let direction = "bottom" export let direction = "bottom"
export let showTip = false export let showTip = false
@ -45,7 +46,7 @@
<Portal target={portalTarget}> <Portal target={portalTarget}>
<div <div
tabindex="0" tabindex="0"
use:positionDropdown={{ anchor, align }} use:positionDropdown={{ anchor, align, maxWidth }}
use:clickOutside={hide} use:clickOutside={hide}
on:keydown={handleEscape} on:keydown={handleEscape}
class={"spectrum-Popover is-open " + (tooltipClasses || "")} class={"spectrum-Popover is-open " + (tooltipClasses || "")}

View File

@ -8,6 +8,8 @@
export let duration = 1000 export let duration = 1000
export let width = false export let width = false
export let sideLabel = false export let sideLabel = false
export let hidePercentage = true
export let color // red, green, default = blue
export let size = "M" export let size = "M"
@ -37,7 +39,7 @@
<slot /> <slot />
</div> </div>
{/if} {/if}
{#if value || value === 0} {#if !hidePercentage && (value || value === 0)}
<div <div
class="spectrum-FieldLabel spectrum-ProgressBar-percentage spectrum-FieldLabel--size{size}" class="spectrum-FieldLabel spectrum-ProgressBar-percentage spectrum-FieldLabel--size{size}"
> >
@ -47,8 +49,19 @@
<div class="spectrum-ProgressBar-track"> <div class="spectrum-ProgressBar-track">
<div <div
class="spectrum-ProgressBar-fill" class="spectrum-ProgressBar-fill"
class:color-green={color === "green"}
class:color-red={color === "red"}
style={value || value === 0 ? `width: ${$progress}%` : ""} style={value || value === 0 ? `width: ${$progress}%` : ""}
/> />
</div> </div>
<div class="spectrum-ProgressBar-label" hidden="" /> <div class="spectrum-ProgressBar-label" hidden="" />
</div> </div>
<style>
.color-green {
background: #009562;
}
.color-red {
background: #dd2019;
}
</style>

View File

@ -1,6 +1,7 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
const multilevel = getContext("sidenav-type") const multilevel = getContext("sidenav-type")
import Badge from "../Badge/Badge.svelte"
export let href = "" export let href = ""
export let external = false export let external = false
export let heading = "" export let heading = ""
@ -8,6 +9,7 @@
export let selected = false export let selected = false
export let disabled = false export let disabled = false
export let dataCy export let dataCy
export let badge = ""
</script> </script>
<li <li
@ -38,10 +40,22 @@
</svg> </svg>
{/if} {/if}
<slot /> <slot />
{#if badge}
<div class="badge">
<Badge active size="S">{badge}</Badge>
</div>
{/if}
</a> </a>
{#if multilevel && $$slots.subnav} {#if multilevel && $$slots.subnav}
<ul class="spectrum-SideNav"> <ul class="spectrum-SideNav">
<slot name="subnav" /> <slot name="subnav" />
</ul> </ul>
{/if} {/if}
</li> </li>
<style>
.badge {
margin-left: 10px;
}
</style>

View File

@ -1,7 +1,14 @@
import { writable } from "svelte/store" import { writable } from "svelte/store"
export const BANNER_TYPES = {
INFO: "info",
NEGATIVE: "negative",
}
export function createBannerStore() { export function createBannerStore() {
const DEFAULT_CONFIG = {} const DEFAULT_CONFIG = {
messages: [],
}
const banner = writable(DEFAULT_CONFIG) const banner = writable(DEFAULT_CONFIG)
@ -20,17 +27,38 @@ export function createBannerStore() {
const showStatus = async () => { const showStatus = async () => {
const config = { const config = {
message: "Some systems are experiencing issues", message: "Some systems are experiencing issues",
type: "negative", type: BANNER_TYPES.NEGATIVE,
extraButtonText: "View Status", extraButtonText: "View Status",
extraButtonAction: () => window.open("https://status.budibase.com/"), extraButtonAction: () => window.open("https://status.budibase.com/"),
} }
await show(config) await queue([config])
}
const queue = async entries => {
const priority = {
[BANNER_TYPES.NEGATIVE]: 0,
[BANNER_TYPES.INFO]: 1,
}
banner.update(store => {
const sorted = [...store.messages, ...entries].sort((a, b) => {
if (priority[a.type] == priority[b.type]) {
return 0
}
return priority[a.type] < priority[b.type] ? -1 : 1
})
return {
...store,
messages: sorted,
}
})
} }
return { return {
subscribe: banner.subscribe, subscribe: banner.subscribe,
showStatus, showStatus,
show,
queue,
} }
} }

View File

@ -4,6 +4,7 @@
export let tooltip = "" export let tooltip = ""
export let size = "M" export let size = "M"
export let disabled = true
let showTooltip = false let showTooltip = false
</script> </script>
@ -19,7 +20,7 @@
on:mouseleave={() => (showTooltip = false)} on:mouseleave={() => (showTooltip = false)}
on:focus on:focus
> >
<Icon name="InfoOutline" size="S" disabled={true} /> <Icon name="InfoOutline" size="S" {disabled} />
</div> </div>
{#if showTooltip} {#if showTooltip}
<div class="tooltip"> <div class="tooltip">
@ -54,7 +55,6 @@
transform: scale(0.75); transform: scale(0.75);
} }
.icon-small { .icon-small {
margin-top: -2px; margin-bottom: -2px;
margin-bottom: -5px;
} }
</style> </style>

View File

@ -34,6 +34,7 @@ export { default as Layout } from "./Layout/Layout.svelte"
export { default as Page } from "./Layout/Page.svelte" export { default as Page } from "./Layout/Page.svelte"
export { default as Link } from "./Link/Link.svelte" export { default as Link } from "./Link/Link.svelte"
export { default as Tooltip } from "./Tooltip/Tooltip.svelte" export { default as Tooltip } from "./Tooltip/Tooltip.svelte"
export { default as TooltipWrapper } from "./Tooltip/TooltipWrapper.svelte"
export { default as Menu } from "./Menu/Menu.svelte" export { default as Menu } from "./Menu/Menu.svelte"
export { default as MenuSection } from "./Menu/Section.svelte" export { default as MenuSection } from "./Menu/Section.svelte"
export { default as MenuSeparator } from "./Menu/Separator.svelte" export { default as MenuSeparator } from "./Menu/Separator.svelte"
@ -94,7 +95,7 @@ export { default as clickOutside } from "./Actions/click_outside"
// Stores // Stores
export { notifications, createNotificationStore } from "./Stores/notifications" export { notifications, createNotificationStore } from "./Stores/notifications"
export { banner } from "./Stores/banner" export { banner, BANNER_TYPES } from "./Stores/banner"
// Helpers // Helpers
export * as Helpers from "./helpers" export * as Helpers from "./helpers"

View File

@ -74,11 +74,11 @@ filterTests(["smoke", "all"], () => {
.contains("Update role") .contains("Update role")
.click({ force: true }) .click({ force: true })
}) })
cy.reload({ timeout: 5000 }) cy.reload()
cy.wait(1000) cy.wait(1000)
} }
// Confirm roles exist within Configure roles table // Confirm roles exist within Configure roles table
cy.get(interact.SPECTRUM_TABLE, { timeout: 2000 }) cy.get(interact.SPECTRUM_TABLE, { timeout: 20000 })
.eq(0) .eq(0)
.within(assginedRoles => { .within(assginedRoles => {
expect(assginedRoles).to.contain("Admin") expect(assginedRoles).to.contain("Admin")
@ -180,7 +180,7 @@ filterTests(["smoke", "all"], () => {
cy.reload() cy.reload()
// Confirm details have been saved // Confirm details have been saved
cy.get(interact.FIELD, { timeout: 1000 }).eq(1).within(() => { cy.get(interact.FIELD, { timeout: 20000 }).eq(1).within(() => {
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', "bb") cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', "bb")
}) })
cy.get(interact.FIELD, { timeout: 1000 }).eq(2).within(() => { cy.get(interact.FIELD, { timeout: 1000 }).eq(2).within(() => {

View File

@ -82,10 +82,10 @@ filterTests(['smoke', 'all'], () => {
}) })
if (Cypress.env("TEST_ENV")) { if (Cypress.env("TEST_ENV")) {
it("should generate data source screens", () => { it("should generate datasource screens", () => {
// Using MySQL data source for testing this // Using MySQL datasource for testing this
const datasource = "MySQL" const datasource = "MySQL"
// Select & configure MySQL data source // Select & configure MySQL datasource
cy.selectExternalDatasource(datasource) cy.selectExternalDatasource(datasource)
cy.addDatasourceConfig(datasource) cy.addDatasourceConfig(datasource)
// Create Autogenerated screens from a MySQL table - MySQL contains books table // Create Autogenerated screens from a MySQL table - MySQL contains books table

View File

@ -10,7 +10,7 @@ filterTests(['smoke', 'all'], () => {
it("should add a current user binding", () => { it("should add a current user binding", () => {
cy.searchAndAddComponent("Paragraph").then(() => { cy.searchAndAddComponent("Paragraph").then(() => {
addSettingBinding("text", "Current User._id") addSettingBinding("text", ["Current User", "_id"], "Current User._id")
}) })
}) })
@ -28,7 +28,7 @@ filterTests(['smoke', 'all'], () => {
const paramName = "foo" const paramName = "foo"
cy.createScreen(`/test/:${paramName}`) cy.createScreen(`/test/:${paramName}`)
cy.searchAndAddComponent("Paragraph").then(componentId => { cy.searchAndAddComponent("Paragraph").then(componentId => {
addSettingBinding("text", `URL.${paramName}`) addSettingBinding("text", ["URL", paramName], `URL.${paramName}`)
// The builder preview pages don't have a real URL, so all we can do // The builder preview pages don't have a real URL, so all we can do
// is check that we were able to bind to the property, and that the // is check that we were able to bind to the property, and that the
// component exists on the page // component exists on the page
@ -47,11 +47,13 @@ filterTests(['smoke', 'all'], () => {
}) })
}) })
const addSettingBinding = (setting, bindingText, clickOption = true) => { const addSettingBinding = (setting, bindingCategories, bindingText, clickOption = true) => {
cy.get(`[data-cy="setting-${setting}"] [data-cy=text-binding-button]`).click() cy.get(`[data-cy="setting-${setting}"] [data-cy=text-binding-button]`).click()
cy.get(".category-list li").contains(bindingCategories[0])
cy.get(".drawer").within(() => { cy.get(".drawer").within(() => {
if (clickOption) { if (clickOption) {
cy.contains(bindingText).click() cy.get(".category-list li").contains(bindingCategories[0]).click()
cy.get("li.binding").contains(bindingCategories[1]).click()
cy.get("textarea").should("have.value", `{{ ${bindingText} }}`) cy.get("textarea").should("have.value", `{{ ${bindingText} }}`)
} else { } else {
cy.get("textarea").type(bindingText) cy.get("textarea").type(bindingText)

View File

@ -2,7 +2,7 @@ import filterTests from "../support/filterTests"
const interact = require("../support/interact") const interact = require("../support/interact")
filterTests(["all"], () => { filterTests(["all"], () => {
context("Create Components", () => { xcontext("Create Components", () => {
let headlineId let headlineId
before(() => { before(() => {
@ -20,7 +20,7 @@ filterTests(["all"], () => {
//Use the tree to delete a selected component //Use the tree to delete a selected component
const deleteSelectedComponent = () => { const deleteSelectedComponent = () => {
cy.get( cy.get(
".nav-items-container .nav-item.selected .actions > div > .icon" ".nav-item.selected .actions > div > .icon"
).click({ ).click({
force: true, force: true,
}) })
@ -91,7 +91,7 @@ filterTests(["all"], () => {
cy.searchAndAddComponent("Paragraph").then(componentId => { cy.searchAndAddComponent("Paragraph").then(componentId => {
cy.get("[data-cy=setting-_instanceName] input").type(componentId).blur() cy.get("[data-cy=setting-_instanceName] input").type(componentId).blur()
cy.get( cy.get(
".nav-items-container .nav-item.selected .actions > div > .icon" ".nav-item.selected .actions > div > .icon"
).click({ ).click({
force: true, force: true,
}) })
@ -145,7 +145,7 @@ filterTests(["all"], () => {
return testFieldFocusOnCreate(label) return testFieldFocusOnCreate(label)
}) })
.then(() => { .then(() => {
cy.get(".nav-items-container .nav-item") cy.get(".nav-item")
.contains(formId) .contains(formId)
.click({ force: true }) .click({ force: true })
deleteSelectedComponent() deleteSelectedComponent()
@ -195,7 +195,7 @@ filterTests(["all"], () => {
return testFocusOnCreate(label) return testFocusOnCreate(label)
}) })
.then(() => { .then(() => {
cy.get(".nav-items-container .nav-item") cy.get(".nav-item")
.contains(providerId) .contains(providerId)
.click({ force: true }) .click({ force: true })
deleteSelectedComponent() deleteSelectedComponent()
@ -218,7 +218,7 @@ filterTests(["all"], () => {
.find(".component-placeholder") .find(".component-placeholder")
.should("not.exist") .should("not.exist")
cy.getComponent(imageId).find(`img[alt=${imageId}]`).should("exist") cy.getComponent(imageId).find(`img[alt=${imageId}]`).should("exist")
cy.get(".nav-items-container .nav-item") cy.get(".nav-item")
.contains(imageId) .contains(imageId)
.click({ force: true }) .click({ force: true })
deleteSelectedComponent() deleteSelectedComponent()
@ -242,7 +242,7 @@ filterTests(["all"], () => {
cy.getComponent(markdownId) cy.getComponent(markdownId)
.find(".editor-preview-full h1") .find(".editor-preview-full h1")
.contains("Hi") .contains("Hi")
cy.get(".nav-items-container .nav-item") cy.get(".nav-item")
.contains(markdownId) .contains(markdownId)
.click({ force: true }) .click({ force: true })
deleteSelectedComponent() deleteSelectedComponent()
@ -265,7 +265,7 @@ filterTests(["all"], () => {
.find(".component-placeholder") .find(".component-placeholder")
.should("not.exist") .should("not.exist")
cy.getComponent(iconId).find("i.ri-save-fill").should("exist") cy.getComponent(iconId).find("i.ri-save-fill").should("exist")
cy.get(".nav-items-container .nav-item") cy.get(".nav-item")
.contains(iconId) .contains(iconId)
.click({ force: true }) .click({ force: true })
deleteSelectedComponent() deleteSelectedComponent()

View File

@ -1,7 +1,7 @@
import filterTests from "../../support/filterTests" import filterTests from "../../support/filterTests"
filterTests(['all'], () => { filterTests(['all'], () => {
context("Datasource Wizard", () => { xcontext("Datasource Wizard", () => {
if (Cypress.env("TEST_ENV")) { if (Cypress.env("TEST_ENV")) {
before(() => { before(() => {
cy.login() cy.login()

View File

@ -11,8 +11,8 @@ filterTests(["all"], () => {
const queryName = "Cypress Test Query" const queryName = "Cypress Test Query"
const queryRename = "CT Query Rename" const queryRename = "CT Query Rename"
it("Should add MySQL data source without configuration", () => { it("Should add MySQL datasource without configuration", () => {
// Select MySQL data source // Select MySQL datasource
cy.selectExternalDatasource(datasource) cy.selectExternalDatasource(datasource)
// Attempt to fetch tables without applying configuration // Attempt to fetch tables without applying configuration
cy.intercept("**/datasources").as("datasource") cy.intercept("**/datasources").as("datasource")
@ -35,8 +35,8 @@ filterTests(["all"], () => {
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true }) cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true })
}) })
it("should add MySQL data source and fetch tables", () => { it("should add MySQL datasource and fetch tables", () => {
// Add & configure MySQL data source // Add & configure MySQL datasource
cy.selectExternalDatasource(datasource) cy.selectExternalDatasource(datasource)
cy.intercept("**/datasources").as("datasource") cy.intercept("**/datasources").as("datasource")
cy.addDatasourceConfig(datasource) cy.addDatasourceConfig(datasource)
@ -52,7 +52,7 @@ filterTests(["all"], () => {
}) })
it("should check table fetching error", () => { it("should check table fetching error", () => {
// MySQL test data source contains tables without primary keys // MySQL test datasource contains tables without primary keys
cy.get(".spectrum-InLineAlert") cy.get(".spectrum-InLineAlert")
.should("contain", "Error fetching tables") .should("contain", "Error fetching tables")
.and("contain", "No primary key constraint found") .and("contain", "No primary key constraint found")

View File

@ -1,7 +1,7 @@
import filterTests from "../../support/filterTests" import filterTests from "../../support/filterTests"
filterTests(["all"], () => { filterTests(["all"], () => {
context("Oracle Datasource Testing", () => { xcontext("Oracle Datasource Testing", () => {
if (Cypress.env("TEST_ENV")) { if (Cypress.env("TEST_ENV")) {
before(() => { before(() => {
cy.login() cy.login()
@ -11,8 +11,8 @@ filterTests(["all"], () => {
const queryName = "Cypress Test Query" const queryName = "Cypress Test Query"
const queryRename = "CT Query Rename" const queryRename = "CT Query Rename"
it("Should add Oracle data source and skip table fetch", () => { it("Should add Oracle datasource and skip table fetch", () => {
// Select Oracle data source // Select Oracle datasource
cy.selectExternalDatasource(datasource) cy.selectExternalDatasource(datasource)
// Skip table fetch - no config added // Skip table fetch - no config added
cy.get(".spectrum-Button") cy.get(".spectrum-Button")
@ -23,7 +23,7 @@ filterTests(["all"], () => {
cy.get(".spectrum-Textfield-input", { timeout: 500 }) cy.get(".spectrum-Textfield-input", { timeout: 500 })
.eq(1) .eq(1)
.should("have.value", "localhost") .should("have.value", "localhost")
// Add another Oracle data source, configure & skip table fetch // Add another Oracle datasource, configure & skip table fetch
cy.selectExternalDatasource(datasource) cy.selectExternalDatasource(datasource)
cy.addDatasourceConfig(datasource, true) cy.addDatasourceConfig(datasource, true)
// Confirm config and no tables // Confirm config and no tables
@ -33,8 +33,8 @@ filterTests(["all"], () => {
cy.get(".spectrum-Body").eq(2).should("contain", "No tables found.") cy.get(".spectrum-Body").eq(2).should("contain", "No tables found.")
}) })
it("Should add Oracle data source and fetch tables without configuration", () => { it("Should add Oracle datasource and fetch tables without configuration", () => {
// Select Oracle data source // Select Oracle datasource
cy.selectExternalDatasource(datasource) cy.selectExternalDatasource(datasource)
// Attempt to fetch tables without applying configuration // Attempt to fetch tables without applying configuration
cy.intercept("**/datasources").as("datasource") cy.intercept("**/datasources").as("datasource")
@ -49,8 +49,8 @@ filterTests(["all"], () => {
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true }) cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true })
}) })
xit("should add Oracle data source and fetch tables", () => { xit("should add Oracle datasource and fetch tables", () => {
// Add & configure Oracle data source // Add & configure Oracle datasource
cy.selectExternalDatasource(datasource) cy.selectExternalDatasource(datasource)
cy.intercept("**/datasources").as("datasource") cy.intercept("**/datasources").as("datasource")
cy.addDatasourceConfig(datasource) cy.addDatasourceConfig(datasource)

View File

@ -11,8 +11,8 @@ filterTests(["all"], () => {
const queryName = "Cypress Test Query" const queryName = "Cypress Test Query"
const queryRename = "CT Query Rename" const queryRename = "CT Query Rename"
xit("Should add PostgreSQL data source without configuration", () => { xit("Should add PostgreSQL datasource without configuration", () => {
// Select PostgreSQL data source // Select PostgreSQL datasource
cy.selectExternalDatasource(datasource) cy.selectExternalDatasource(datasource)
// Attempt to fetch tables without applying configuration // Attempt to fetch tables without applying configuration
cy.intercept("**/datasources").as("datasource") cy.intercept("**/datasources").as("datasource")
@ -27,8 +27,8 @@ filterTests(["all"], () => {
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true }) cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true })
}) })
it("should add PostgreSQL data source and fetch tables", () => { it("should add PostgreSQL datasource and fetch tables", () => {
// Add & configure PostgreSQL data source // Add & configure PostgreSQL datasource
cy.selectExternalDatasource(datasource) cy.selectExternalDatasource(datasource)
cy.intercept("**/datasources").as("datasource") cy.intercept("**/datasources").as("datasource")
cy.addDatasourceConfig(datasource) cy.addDatasourceConfig(datasource)
@ -162,7 +162,7 @@ filterTests(["all"], () => {
switchSchema("randomText") switchSchema("randomText")
// No tables displayed // No tables displayed
cy.get(".spectrum-Body", { timeout: 5000 }).eq(2).should("contain", "No tables found") cy.get(".spectrum-Body", { timeout: 10000 }).eq(2, { timeout: 10000 }).should("contain", "No tables found")
// Previously created query should be visible // Previously created query should be visible
cy.get(".spectrum-Table").should("contain", queryName) cy.get(".spectrum-Table").should("contain", queryName)
@ -173,7 +173,7 @@ filterTests(["all"], () => {
switchSchema("1") switchSchema("1")
// Confirm tables exist - Check for specific one // Confirm tables exist - Check for specific one
cy.get(".spectrum-Table", { timeout: 5000 }).eq(0).should("contain", "test") cy.get(".spectrum-Table", { timeout: 20000 }).eq(0).should("contain", "test")
cy.get(".spectrum-Table") cy.get(".spectrum-Table")
.eq(0) .eq(0)
.find(".spectrum-Table-row") .find(".spectrum-Table-row")
@ -187,7 +187,7 @@ filterTests(["all"], () => {
switchSchema("public") switchSchema("public")
// Confirm tables exist - again // Confirm tables exist - again
cy.get(".spectrum-Table", { timeout: 5000 }).eq(0).should("contain", "REGIONS") cy.get(".spectrum-Table", { timeout: 20000 }).eq(0).should("contain", "REGIONS")
cy.get(".spectrum-Table") cy.get(".spectrum-Table")
.eq(0) .eq(0)
.find(".spectrum-Table-row") .find(".spectrum-Table-row")

View File

@ -10,11 +10,11 @@ filterTests(["smoke", "all"], () => {
const datasource = "REST" const datasource = "REST"
const restUrl = "https://api.openbrewerydb.org/breweries" const restUrl = "https://api.openbrewerydb.org/breweries"
it("Should add REST data source with incorrect API", () => { it("Should add REST datasource with incorrect API", () => {
// Select REST data source // Select REST datasource
cy.selectExternalDatasource(datasource) cy.selectExternalDatasource(datasource)
// Enter incorrect api & attempt to send query // Enter incorrect api & attempt to send query
cy.get(".spectrum-Button", { timeout: 500 }).contains("Add query").click({ force: true }) cy.get(".query-buttons", { timeout: 1000 }).contains("Add query").click({ force: true })
cy.intercept("**/preview").as("queryError") cy.intercept("**/preview").as("queryError")
cy.get("input").clear().type("random text") cy.get("input").clear().type("random text")
cy.get(".spectrum-Button").contains("Send").click({ force: true }) cy.get(".spectrum-Button").contains("Send").click({ force: true })

View File

@ -4,7 +4,7 @@ Cypress.on("uncaught:exception", () => {
// ACCOUNTS & USERS // ACCOUNTS & USERS
Cypress.Commands.add("login", (email, password) => { Cypress.Commands.add("login", (email, password) => {
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 10000 }) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 30000 })
cy.url() cy.url()
.should("include", "/builder/") .should("include", "/builder/")
.then(url => { .then(url => {
@ -33,7 +33,7 @@ Cypress.Commands.add("login", (email, password) => {
}) })
Cypress.Commands.add("logOut", () => { Cypress.Commands.add("logOut", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 2000 }) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 30000 })
cy.get(".user-dropdown .avatar > .icon").click({ force: true }) cy.get(".user-dropdown .avatar > .icon").click({ force: true })
cy.get(".spectrum-Popover[data-cy='user-menu']").within(() => { cy.get(".spectrum-Popover[data-cy='user-menu']").within(() => {
cy.get("li[data-cy='user-logout']").click({ force: true }) cy.get("li[data-cy='user-logout']").click({ force: true })
@ -43,7 +43,7 @@ Cypress.Commands.add("logOut", () => {
Cypress.Commands.add("logoutNoAppGrid", () => { Cypress.Commands.add("logoutNoAppGrid", () => {
// Logs user out when app grid is not present // Logs user out when app grid is not present
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 30000 })
cy.get(".avatar > .icon").click({ force: true }) cy.get(".avatar > .icon").click({ force: true })
cy.get(".spectrum-Popover[data-cy='user-menu']").within(() => { cy.get(".spectrum-Popover[data-cy='user-menu']").within(() => {
cy.get(".spectrum-Menu-item").contains("Log out").click({ force: true }) cy.get(".spectrum-Menu-item").contains("Log out").click({ force: true })
@ -68,11 +68,14 @@ Cypress.Commands.add("createUser", (email, permission) => {
.click({ force: true }) .click({ force: true })
}) })
} }
// Add user and wait for modal to change // Add user
cy.get(".spectrum-Button").contains("Add user").click({ force: true }) cy.get(".spectrum-Button").contains("Add users").click({ force: true })
cy.get(".spectrum-ActionButton").contains("Add email").should("not.exist") cy.get(".spectrum-ActionButton").contains("Add email").should("not.exist")
}) })
// Onboarding modal // Onboarding modal
cy.get(".spectrum-Dialog-grid", { timeout: 5000 }).contains(
"Choose your onboarding"
)
cy.get(".spectrum-Dialog-grid").within(() => { cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".onboarding-type").eq(1).click() cy.get(".onboarding-type").eq(1).click()
cy.get(".spectrum-Button").contains("Done").click({ force: true }) cy.get(".spectrum-Button").contains("Done").click({ force: true })
@ -163,7 +166,7 @@ Cypress.Commands.add("createApp", (name, addDefaultTable) => {
const shouldCreateDefaultTable = const shouldCreateDefaultTable =
typeof addDefaultTable != "boolean" ? true : addDefaultTable typeof addDefaultTable != "boolean" ? true : addDefaultTable
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 10000 }) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 30000 })
cy.url({ timeout: 30000 }).should("include", "/apps") cy.url({ timeout: 30000 }).should("include", "/apps")
cy.get(`[data-cy="create-app-btn"]`, { timeout: 5000 }).click({ force: true }) cy.get(`[data-cy="create-app-btn"]`, { timeout: 5000 }).click({ force: true })
@ -197,7 +200,7 @@ Cypress.Commands.add("createApp", (name, addDefaultTable) => {
}) })
Cypress.Commands.add("deleteApp", name => { Cypress.Commands.add("deleteApp", name => {
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 30000 })
cy.wait(2000) cy.wait(2000)
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body") .its("body")
@ -254,7 +257,7 @@ Cypress.Commands.add("deleteApp", name => {
}) })
Cypress.Commands.add("deleteAllApps", () => { Cypress.Commands.add("deleteAllApps", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 30000 })
cy.wait(500) cy.wait(500)
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`, { cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`, {
timeout: 5000, timeout: 5000,
@ -351,7 +354,7 @@ Cypress.Commands.add("alterAppVersion", (appId, version) => {
}) })
Cypress.Commands.add("importApp", (exportFilePath, name) => { Cypress.Commands.add("importApp", (exportFilePath, name) => {
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 30000 })
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body") .its("body")
@ -386,7 +389,7 @@ Cypress.Commands.add("importApp", (exportFilePath, name) => {
// Filters visible with 1 or more // Filters visible with 1 or more
Cypress.Commands.add("searchForApplication", appName => { Cypress.Commands.add("searchForApplication", appName => {
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 30000 })
cy.wait(2000) cy.wait(2000)
// No app filter functionality if only 1 app exists // No app filter functionality if only 1 app exists
@ -409,7 +412,7 @@ Cypress.Commands.add("searchForApplication", appName => {
// Assumes there are no others // Assumes there are no others
Cypress.Commands.add("applicationInAppTable", appName => { Cypress.Commands.add("applicationInAppTable", appName => {
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 10000 }) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 30000 })
cy.get(".appTable", { timeout: 5000 }).within(() => { cy.get(".appTable", { timeout: 5000 }).within(() => {
cy.get(".title").contains(appName).should("exist") cy.get(".title").contains(appName).should("exist")
}) })
@ -454,8 +457,8 @@ Cypress.Commands.add("createTable", (tableName, initialTable) => {
cy.get(".spectrum-ButtonGroup").contains("Create").click() cy.get(".spectrum-ButtonGroup").contains("Create").click()
}) })
// Ensure modal has closed and table is created // Ensure modal has closed and table is created
cy.get(".spectrum-Modal").should("not.exist") cy.get(".spectrum-Modal", { timeout: 2000 }).should("not.exist")
cy.get(".spectrum-Tabs-content", { timeout: 1000 }).should( cy.get(".spectrum-Tabs-content", { timeout: 2000 }).should(
"contain", "contain",
tableName tableName
) )
@ -634,30 +637,32 @@ Cypress.Commands.add(
(datasourceNames, accessLevelLabel) => { (datasourceNames, accessLevelLabel) => {
cy.contains("Design").click() cy.contains("Design").click()
cy.get(".spectrum-Button").contains("Add screen").click({ force: true }) cy.get(".spectrum-Button").contains("Add screen").click({ force: true })
cy.get(".spectrum-Modal").within(() => { cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".item").contains("Autogenerated screens").click() cy.get("[data-cy='autogenerated-screens']").click()
cy.intercept("**/api/datasources").as("autoScreens")
cy.get(".spectrum-Button").contains("Continue").click({ force: true }) cy.get(".spectrum-Button").contains("Continue").click({ force: true })
cy.wait("@autoScreens")
cy.wait(5000)
}) })
cy.get(".spectrum-Modal [data-cy='data-source-modal']", { cy.get("[data-cy='autogenerated-screens']").should("not.exist")
timeout: 500, cy.get("[data-cy='data-source-modal']", { timeout: 10000 }).within(() => {
}).within(() => {
for (let i = 0; i < datasourceNames.length; i++) { for (let i = 0; i < datasourceNames.length; i++) {
cy.wait(500) cy.get(".data-source-entry")
cy.get(".data-source-entry").contains(datasourceNames[i]).click() .contains(datasourceNames[i], { timeout: 20000 })
//Ensure the check mark is visible .click({ force: true })
// Ensure the check mark is visible
cy.get(".data-source-entry") cy.get(".data-source-entry")
.contains(datasourceNames[i]) .contains(datasourceNames[i])
.get(".data-source-check") .get(".data-source-check", { timeout: 20000 })
.should("exist") .should("exist")
} }
cy.get(".spectrum-Button").contains("Confirm").click({ force: true }) cy.get(".spectrum-Button").contains("Confirm").click({ force: true })
}) })
cy.get(".spectrum-Modal").within(() => { cy.get(".spectrum-Modal", { timeout: 10000 }).within(() => {
if (accessLevelLabel) { if (accessLevelLabel) {
cy.get(".spectrum-Picker-label").click() cy.get(".spectrum-Picker-label", { timeout: 10000 }).click()
cy.wait(500)
cy.contains(accessLevelLabel).click() cy.contains(accessLevelLabel).click()
} }
cy.get(".spectrum-Button").contains("Done").click({ force: true }) cy.get(".spectrum-Button").contains("Done").click({ force: true })
@ -758,7 +763,7 @@ Cypress.Commands.add("navigateToDataSection", () => {
}) })
Cypress.Commands.add("navigateToAutogeneratedModal", () => { Cypress.Commands.add("navigateToAutogeneratedModal", () => {
// Screen name must already exist within data source // Screen name must already exist within datasource
cy.contains("Design").click() cy.contains("Design").click()
cy.get(".spectrum-Button").contains("Add screen").click({ force: true }) cy.get(".spectrum-Button").contains("Add screen").click({ force: true })
cy.get(".spectrum-Modal").within(() => { cy.get(".spectrum-Modal").within(() => {
@ -774,7 +779,7 @@ Cypress.Commands.add("navigateToAutogeneratedModal", () => {
Cypress.Commands.add("selectExternalDatasource", datasourceName => { Cypress.Commands.add("selectExternalDatasource", datasourceName => {
// Navigates to Data Section // Navigates to Data Section
cy.navigateToDataSection() cy.navigateToDataSection()
// Open Data Source modal // Open Datasource modal
cy.get(".nav").within(() => { cy.get(".nav").within(() => {
cy.get(".add-button").click() cy.get(".add-button").click()
}) })
@ -912,8 +917,9 @@ Cypress.Commands.add("createRestQuery", (method, restUrl, queryPrettyName) => {
Cypress.Commands.add("closeModal", () => { Cypress.Commands.add("closeModal", () => {
cy.get(".spectrum-Modal", { timeout: 2000 }).within(() => { cy.get(".spectrum-Modal", { timeout: 2000 }).within(() => {
cy.get(".close-icon").click() cy.get(".close-icon").click()
cy.wait(1000) // Wait for modal to close
}) })
// Confirm modal has closed
cy.get(".spectrum-Modal", { timeout: 10000 }).should("not.exist")
}) })
Cypress.Commands.add("expandBudibaseConnection", () => { Cypress.Commands.add("expandBudibaseConnection", () => {

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/builder", "name": "@budibase/builder",
"version": "1.3.21", "version": "1.3.22-alpha.4",
"license": "GPL-3.0", "license": "GPL-3.0",
"private": true, "private": true,
"scripts": { "scripts": {
@ -71,10 +71,10 @@
} }
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.3.21", "@budibase/bbui": "1.3.22-alpha.4",
"@budibase/client": "^1.3.21", "@budibase/client": "1.3.22-alpha.4",
"@budibase/frontend-core": "^1.3.21", "@budibase/frontend-core": "1.3.22-alpha.4",
"@budibase/string-templates": "^1.3.21", "@budibase/string-templates": "1.3.22-alpha.4",
"@sentry/browser": "5.19.1", "@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1", "@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1", "@spectrum-css/vars": "^3.0.1",
@ -98,7 +98,7 @@
"@babel/runtime": "^7.13.10", "@babel/runtime": "^7.13.10",
"@rollup/plugin-replace": "^2.4.2", "@rollup/plugin-replace": "^2.4.2",
"@roxi/routify": "2.18.5", "@roxi/routify": "2.18.5",
"@sveltejs/vite-plugin-svelte": "1.0.0-next.19", "@sveltejs/vite-plugin-svelte": "1.0.1",
"@testing-library/jest-dom": "^5.11.10", "@testing-library/jest-dom": "^5.11.10",
"@testing-library/svelte": "^3.0.0", "@testing-library/svelte": "^3.0.0",
"babel-jest": "^26.6.3", "babel-jest": "^26.6.3",
@ -120,7 +120,7 @@
"ts-node": "^10.4.0", "ts-node": "^10.4.0",
"tsconfig-paths": "4.0.0", "tsconfig-paths": "4.0.0",
"typescript": "^4.5.5", "typescript": "^4.5.5",
"vite": "^2.1.5" "vite": "^3.0.8"
}, },
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072" "gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
} }

View File

@ -4,6 +4,7 @@
import { NotificationDisplay, BannerDisplay } from "@budibase/bbui" import { NotificationDisplay, BannerDisplay } from "@budibase/bbui"
import { parse, stringify } from "qs" import { parse, stringify } from "qs"
import HelpIcon from "components/common/HelpIcon.svelte" import HelpIcon from "components/common/HelpIcon.svelte"
import LicensingOverlays from "components/portal/licensing/LicensingOverlays.svelte"
const queryHandler = { parse, stringify } const queryHandler = { parse, stringify }
</script> </script>
@ -12,6 +13,9 @@
<BannerDisplay /> <BannerDisplay />
<NotificationDisplay /> <NotificationDisplay />
<LicensingOverlays />
<Router {routes} config={{ queryHandler }} /> <Router {routes} config={{ queryHandler }} />
<div class="modal-container" /> <div class="modal-container" />
<HelpIcon /> <HelpIcon />

View File

@ -297,7 +297,10 @@ const getProviderContextBindings = (asset, dataProviders) => {
schema = {} schema = {}
const values = context.values || [] const values = context.values || []
values.forEach(value => { values.forEach(value => {
schema[value.key] = { name: value.label, type: "string" } schema[value.key] = {
name: value.label,
type: value.type || "string",
}
}) })
} else if (context.type === "schema") { } else if (context.type === "schema") {
// Schema contexts are generated dynamically depending on their data // Schema contexts are generated dynamically depending on their data
@ -357,6 +360,12 @@ const getProviderContextBindings = (asset, dataProviders) => {
providerId, providerId,
// Table ID is used by JSON fields to know what table the field is in // Table ID is used by JSON fields to know what table the field is in
tableId: table?._id, tableId: table?._id,
category: component._instanceName,
icon: def.icon,
display: {
name: fieldSchema.name || key,
type: fieldSchema.type,
},
}) })
}) })
}) })
@ -383,6 +392,9 @@ export const getUserBindings = () => {
// datasource options, based on bindable properties // datasource options, based on bindable properties
fieldSchema, fieldSchema,
providerId: "user", providerId: "user",
category: "Current User",
icon: "User",
display: fieldSchema,
}) })
}) })
return bindings return bindings
@ -399,11 +411,17 @@ const getDeviceBindings = () => {
type: "context", type: "context",
runtimeBinding: `${safeDevice}.${makePropSafe("mobile")}`, runtimeBinding: `${safeDevice}.${makePropSafe("mobile")}`,
readableBinding: `Device.Mobile`, readableBinding: `Device.Mobile`,
category: "Device",
icon: "DevicePhone",
display: { type: "boolean", name: "mobile" },
}) })
bindings.push({ bindings.push({
type: "context", type: "context",
runtimeBinding: `${safeDevice}.${makePropSafe("tablet")}`, runtimeBinding: `${safeDevice}.${makePropSafe("tablet")}`,
readableBinding: `Device.Tablet`, readableBinding: `Device.Tablet`,
category: "Device",
icon: "DevicePhone",
display: { type: "boolean", name: "tablet" },
}) })
} }
return bindings return bindings
@ -427,6 +445,8 @@ const getSelectedRowsBindings = asset => {
"selectedRows" "selectedRows"
)}`, )}`,
readableBinding: `${table._instanceName}.Selected rows`, readableBinding: `${table._instanceName}.Selected rows`,
category: "Selected rows",
icon: "ViewRow",
})) }))
) )
@ -458,6 +478,9 @@ const getStateBindings = () => {
type: "context", type: "context",
runtimeBinding: `${safeState}.${makePropSafe(key)}`, runtimeBinding: `${safeState}.${makePropSafe(key)}`,
readableBinding: `State.${key}`, readableBinding: `State.${key}`,
category: "State",
icon: "AutomatedSegment",
display: { name: key },
})) }))
} }
return bindings return bindings
@ -480,11 +503,17 @@ const getUrlBindings = asset => {
type: "context", type: "context",
runtimeBinding: `${safeURL}.${makePropSafe(param)}`, runtimeBinding: `${safeURL}.${makePropSafe(param)}`,
readableBinding: `URL.${param}`, readableBinding: `URL.${param}`,
category: "URL",
icon: "RailTop",
display: { type: "string" },
})) }))
const queryParamsBinding = { const queryParamsBinding = {
type: "context", type: "context",
runtimeBinding: makePropSafe("query"), runtimeBinding: makePropSafe("query"),
readableBinding: "Query params", readableBinding: "Query params",
category: "URL",
icon: "RailTop",
display: { type: "object" },
} }
return urlParamBindings.concat([queryParamsBinding]) return urlParamBindings.concat([queryParamsBinding])
} }
@ -495,6 +524,9 @@ const getRoleBindings = () => {
type: "context", type: "context",
runtimeBinding: `trim "${role._id}"`, runtimeBinding: `trim "${role._id}"`,
readableBinding: `Role.${role.name}`, readableBinding: `Role.${role.name}`,
category: "Role",
icon: "UserGroup",
display: { type: "string", name: role.name },
} }
}) })
} }
@ -516,6 +548,7 @@ export const getEventContextBindings = (
// Check if any context bindings are provided by the component for this // Check if any context bindings are provided by the component for this
// setting // setting
const component = findComponent(asset.props, componentId) const component = findComponent(asset.props, componentId)
const def = store.actions.components.getDefinition(component?._component)
const settings = getComponentSettings(component?._component) const settings = getComponentSettings(component?._component)
const eventSetting = settings.find(setting => setting.key === settingKey) const eventSetting = settings.find(setting => setting.key === settingKey)
if (eventSetting?.context?.length) { if (eventSetting?.context?.length) {
@ -525,6 +558,8 @@ export const getEventContextBindings = (
runtimeBinding: `${makePropSafe("eventContext")}.${makePropSafe( runtimeBinding: `${makePropSafe("eventContext")}.${makePropSafe(
contextEntry.key contextEntry.key
)}`, )}`,
category: component._instanceName,
icon: def.icon,
}) })
}) })
} }
@ -546,6 +581,8 @@ export const getEventContextBindings = (
bindings.push({ bindings.push({
readableBinding: `Action ${idx + 1}.${contextValue.label}`, readableBinding: `Action ${idx + 1}.${contextValue.label}`,
runtimeBinding: `actions.${idx}.${contextValue.value}`, runtimeBinding: `actions.${idx}.${contextValue.value}`,
category: "Actions",
icon: "JourneyAction",
}) })
}) })
} }

View File

@ -9,7 +9,7 @@ function prepareData(config) {
ds => ds.source === config.type ds => ds.source === config.type
).length ).length
let baseName = IntegrationNames[config.type] let baseName = IntegrationNames[config.type] || config.name
let name = let name =
existingTypeCount === 0 ? baseName : `${baseName}-${existingTypeCount + 1}` existingTypeCount === 0 ? baseName : `${baseName}-${existingTypeCount + 1}`
@ -27,7 +27,7 @@ export async function saveDatasource(config, skipFetch = false) {
// Create datasource // Create datasource
const resp = await datasources.save(datasource, !skipFetch && datasource.plus) const resp = await datasources.save(datasource, !skipFetch && datasource.plus)
// update the tables incase data source plus // update the tables incase datasource plus
await tables.fetch() await tables.fetch()
await datasources.select(resp._id) await datasources.select(resp._id)
return resp return resp

View File

@ -1,5 +1,6 @@
import { getFrontendStore } from "./store/frontend" import { getFrontendStore } from "./store/frontend"
import { getAutomationStore } from "./store/automation" import { getAutomationStore } from "./store/automation"
import { getTemporalStore } from "./store/temporal"
import { getThemeStore } from "./store/theme" import { getThemeStore } from "./store/theme"
import { derived } from "svelte/store" import { derived } from "svelte/store"
import { findComponent, findComponentPath } from "./componentUtils" import { findComponent, findComponentPath } from "./componentUtils"
@ -8,6 +9,7 @@ import { RoleUtils } from "@budibase/frontend-core"
export const store = getFrontendStore() export const store = getFrontendStore()
export const automationStore = getAutomationStore() export const automationStore = getAutomationStore()
export const themeStore = getThemeStore() export const themeStore = getThemeStore()
export const temporalStore = getTemporalStore()
export const selectedScreen = derived(store, $store => { export const selectedScreen = derived(store, $store => {
return $store.screens.find(screen => screen._id === $store.selectedScreenId) return $store.screens.find(screen => screen._id === $store.selectedScreenId)

View File

@ -90,13 +90,21 @@ export const getFrontendStore = () => {
// Fetch component definitions. // Fetch component definitions.
// Allow errors to propagate. // Allow errors to propagate.
let components = await API.fetchComponentLibDefinitions(application.appId) const components = await API.fetchComponentLibDefinitions(
application.appId
)
// Filter out custom component keys so we can flag them
const customComponents = Object.keys(components).filter(name =>
name.startsWith("plugin/")
)
// Reset store state // Reset store state
store.update(state => ({ store.update(state => ({
...state, ...state,
libraries: application.componentLibraries, libraries: application.componentLibraries,
components, components,
customComponents,
clientFeatures: { clientFeatures: {
...INITIAL_FRONTEND_STATE.clientFeatures, ...INITIAL_FRONTEND_STATE.clientFeatures,
...components.features, ...components.features,
@ -116,6 +124,7 @@ export const getFrontendStore = () => {
version: application.version, version: application.version,
revertableVersion: application.revertableVersion, revertableVersion: application.revertableVersion,
navigation: application.navigation || {}, navigation: application.navigation || {},
usedPlugins: application.usedPlugins || [],
})) }))
// Initialise backend stores // Initialise backend stores
@ -189,9 +198,18 @@ export const getFrontendStore = () => {
}) })
}, },
save: async screen => { save: async screen => {
const state = get(store)
const creatingNewScreen = screen._id === undefined const creatingNewScreen = screen._id === undefined
const savedScreen = await API.saveScreen(screen) const savedScreen = await API.saveScreen(screen)
const routesResponse = await API.fetchAppRoutes() const routesResponse = await API.fetchAppRoutes()
let usedPlugins = state.usedPlugins
// If plugins changed we need to fetch the latest app metadata
if (savedScreen.pluginAdded) {
const { application } = await API.fetchAppPackage(state.appId)
usedPlugins = application.usedPlugins || []
}
store.update(state => { store.update(state => {
// Update screen object // Update screen object
const idx = state.screens.findIndex(x => x._id === savedScreen._id) const idx = state.screens.findIndex(x => x._id === savedScreen._id)
@ -210,6 +228,9 @@ export const getFrontendStore = () => {
// Update routes // Update routes
state.routes = routesResponse.routes state.routes = routesResponse.routes
// Update used plugins
state.usedPlugins = usedPlugins
return state return state
}) })
return savedScreen return savedScreen
@ -368,9 +389,6 @@ export const getFrontendStore = () => {
if (!componentName) { if (!componentName) {
return null return null
} }
if (!componentName.startsWith("@budibase")) {
componentName = `@budibase/standard-components/${componentName}`
}
return get(store).components[componentName] return get(store).components[componentName]
}, },
createInstance: (componentName, presetProps) => { createInstance: (componentName, presetProps) => {

View File

@ -0,0 +1,43 @@
import { createLocalStorageStore } from "@budibase/frontend-core"
import { get } from "svelte/store"
export const getTemporalStore = () => {
const initialValue = {}
const localStorageKey = `bb-temporal`
const store = createLocalStorageStore(localStorageKey, initialValue)
const setExpiring = (key, data, duration) => {
const updated = {
...data,
expiry: Date.now() + duration * 1000,
}
store.update(state => ({
...state,
[key]: updated,
}))
}
const getExpiring = key => {
const entry = get(store)[key]
if (!entry) {
return
}
const currentExpiry = entry.expiry
if (currentExpiry < Date.now()) {
store.update(state => {
delete state[key]
return state
})
return null
} else {
return entry
}
}
return {
subscribe: store.subscribe,
actions: { setExpiring, getExpiring },
}
}

View File

@ -13,7 +13,7 @@
customQueryIconColor, customQueryIconColor,
customQueryText, customQueryText,
} from "helpers/data/utils" } from "helpers/data/utils"
import ICONS from "./icons" import { getIcon } from "./icons"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
let openDataSources = [] let openDataSources = []
@ -32,8 +32,8 @@
: [] : []
$: openDataSource = enrichedDataSources.find(x => x.open) $: openDataSource = enrichedDataSources.find(x => x.open)
$: { $: {
// Ensure the open data source is always included in the list of open // Ensure the open datasource is always included in the list of open
// data sources // datasources
if (openDataSource) { if (openDataSource) {
openNode(openDataSource) openNode(openDataSource)
} }
@ -79,7 +79,7 @@
}) })
const containsActiveEntity = datasource => { const containsActiveEntity = datasource => {
// If we're view a query then the data source ID is in the URL // If we're view a query then the datasource ID is in the URL
if ($params.selectedDatasource === datasource._id) { if ($params.selectedDatasource === datasource._id) {
return true return true
} }
@ -124,7 +124,7 @@
> >
<div class="datasource-icon" slot="icon"> <div class="datasource-icon" slot="icon">
<svelte:component <svelte:component
this={ICONS[datasource.source]} this={getIcon(datasource.source, datasource.schema)}
height="18" height="18"
width="18" width="18"
/> />

View File

@ -8,6 +8,7 @@
notifications, notifications,
Modal, Modal,
Table, Table,
Toggle,
} from "@budibase/bbui" } from "@budibase/bbui"
import { datasources, integrations, tables } from "stores/backend" import { datasources, integrations, tables } from "stores/backend"
import CreateEditRelationship from "components/backend/Datasources/CreateEditRelationship.svelte" import CreateEditRelationship from "components/backend/Datasources/CreateEditRelationship.svelte"
@ -15,6 +16,7 @@
import ArrayRenderer from "components/common/renderers/ArrayRenderer.svelte" import ArrayRenderer from "components/common/renderers/ArrayRenderer.svelte"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import ValuesList from "components/common/ValuesList.svelte"
export let datasource export let datasource
export let save export let save
@ -31,6 +33,8 @@
let createExternalTableModal let createExternalTableModal
let selectedFromRelationship, selectedToRelationship let selectedFromRelationship, selectedToRelationship
let confirmDialog let confirmDialog
let specificTables = null
let requireSpecificTables = false
$: integration = datasource && $integrations[datasource.source] $: integration = datasource && $integrations[datasource.source]
$: plusTables = datasource?.plus $: plusTables = datasource?.plus
@ -87,7 +91,7 @@
async function updateDatasourceSchema() { async function updateDatasourceSchema() {
try { try {
await datasources.updateSchema(datasource) await datasources.updateSchema(datasource, specificTables)
notifications.success(`Datasource ${name} tables updated successfully.`) notifications.success(`Datasource ${name} tables updated successfully.`)
await tables.fetch() await tables.fetch()
} catch (error) { } catch (error) {
@ -150,6 +154,19 @@
warning={false} warning={false}
title="Confirm table fetch" title="Confirm table fetch"
> >
<Toggle
bind:value={requireSpecificTables}
on:change={e => {
requireSpecificTables = e.detail
specificTables = null
}}
thin
text="Fetch listed tables only (one per line)"
/>
{#if requireSpecificTables}
<ValuesList label="" bind:values={specificTables} />
{/if}
<br />
<Body> <Body>
If you have fetched tables from this database before, this action may If you have fetched tables from this database before, this action may
overwrite any changes you made after your initial fetch. overwrite any changes you made after your initial fetch.

View File

@ -0,0 +1,65 @@
<script>
import { createEventDispatcher } from "svelte"
import { Heading, Detail } from "@budibase/bbui"
import { getIcon } from "../icons"
export let integration
export let integrationType
export let schema
let dispatcher = createEventDispatcher()
</script>
<div
class:selected={integration.type === integrationType}
on:click={() => dispatcher("selected", integrationType)}
class="item hoverable"
>
<div class="item-body" class:with-type={!!schema.type}>
<svelte:component
this={getIcon(integrationType, schema)}
height="20"
width="20"
/>
<div class="text">
<Heading size="XXS">{schema.friendlyName}</Heading>
{#if schema.type}
<Detail size="S">{schema.type || ""}</Detail>
{/if}
</div>
</div>
</div>
<style>
.item {
cursor: pointer;
display: grid;
grid-gap: var(--spectrum-alias-grid-margin-xsmall);
padding: var(--spectrum-alias-item-padding-s)
var(--spectrum-alias-item-padding-m);
background: var(--spectrum-alias-background-color-secondary);
transition: background 0.13s ease-out;
border: solid var(--spectrum-alias-border-color);
border-radius: 5px;
box-sizing: border-box;
border-width: 2px;
}
.item:hover,
.item.selected {
background: var(--spectrum-alias-background-color-tertiary);
}
.item-body {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-m);
}
.item-body.with-type {
align-items: flex-start;
}
.item-body.with-type :global(svg) {
margin-top: 4px;
}
</style>

View File

@ -0,0 +1,52 @@
<script>
export let width = "100"
export let height = "100"
</script>
<svg
{width}
{height}
xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
viewBox="0 0 18.43 17.62"
xml:space="preserve"
>
<style type="text/css">
.custom-st0 {
fill: var(
--spectrum-heading-xxs-text-color,
var(--spectrum-alias-heading-text-color)
);
}
</style>
<ellipse
id="Ellipse_9514"
class="custom-st0"
cx="9.14"
cy="2.88"
rx="8"
ry="2.5"
/>
<path
id="Path_8355"
class="custom-st0"
d="M7.64,12.88c0-0.48,0.06-0.95,0.17-1.41c-2.53-0.16-5.92-0.76-6.67-1.95v4.36
c0,1.34,3.39,2.43,7.63,2.49C8.03,15.36,7.63,14.13,7.64,12.88z"
/>
<path
id="Path_8356"
class="custom-st0"
d="M13.64,6.88c1.25,0,2.47,0.39,3.48,1.12c0.01-0.04,0.02-0.08,0.02-0.12V4.51
c-1.22,1.55-5.53,2-8,2s-7.11-0.58-8-2v3.36c0,1.28,3.09,2.33,7.06,2.48C9.18,8.24,11.3,6.88,13.64,6.88z"
/>
<path
id="Path_8353"
class="custom-st0"
d="M10.32,14.42c-0.56-0.56-0.56-1.47,0-2.03l1.24-1.24l-0.01-0.01c-0.25-0.25-0.25-0.65,0-0.9l0,0
L12,9.79c0.25-0.25,0.65-0.25,0.9,0c0,0,0,0,0,0l0,0l0.45,0.45l1.53-1.53c0.09-0.09,0.24-0.09,0.34,0l0.34,0.34
c0.09,0.09,0.09,0.24,0,0.34l-1.53,1.53l1.35,1.35l1.53-1.53c0.09-0.09,0.24-0.09,0.34,0l0.34,0.34c0.09,0.09,0.09,0.24,0,0.34
l-1.53,1.53l0.45,0.45c0.25,0.25,0.25,0.65,0,0.9l0,0l-0.45,0.45c-0.25,0.25-0.65,0.25-0.9,0l0,0l-0.01-0.01l-1.24,1.24
c-0.56,0.56-1.47,0.56-2.03,0l0,0L10.32,14.42z"
/>
</svg>

View File

@ -15,8 +15,9 @@ import GoogleSheets from "./GoogleSheets.svelte"
import Firebase from "./Firebase.svelte" import Firebase from "./Firebase.svelte"
import Redis from "./Redis.svelte" import Redis from "./Redis.svelte"
import Snowflake from "./Snowflake.svelte" import Snowflake from "./Snowflake.svelte"
import Custom from "./Custom.svelte"
export default { const ICONS = {
BUDIBASE: Budibase, BUDIBASE: Budibase,
POSTGRES: Postgres, POSTGRES: Postgres,
DYNAMODB: DynamoDB, DYNAMODB: DynamoDB,
@ -34,4 +35,15 @@ export default {
FIRESTORE: Firebase, FIRESTORE: Firebase,
REDIS: Redis, REDIS: Redis,
SNOWFLAKE: Snowflake, SNOWFLAKE: Snowflake,
CUSTOM: Custom,
}
export default ICONS
export function getIcon(integrationType, schema) {
if (schema?.custom || !ICONS[integrationType]) {
return ICONS.CUSTOM
} else {
return ICONS[integrationType]
}
} }

View File

@ -18,6 +18,7 @@
import { createRestDatasource } from "builderStore/datasource" import { createRestDatasource } from "builderStore/datasource"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import ImportRestQueriesModal from "./ImportRestQueriesModal.svelte" import ImportRestQueriesModal from "./ImportRestQueriesModal.svelte"
import DatasourceCard from "../_components/DatasourceCard.svelte"
export let modal export let modal
let integrations = {} let integrations = {}
@ -27,6 +28,9 @@
let importModal let importModal
$: showImportButton = false $: showImportButton = false
$: customIntegrations = Object.entries(integrations).filter(
entry => entry[1].custom
)
checkShowImport() checkShowImport()
@ -49,6 +53,9 @@
schema: selected.datasource, schema: selected.datasource,
auth: selected.auth, auth: selected.auth,
} }
if (selected.friendlyName) {
integration.name = selected.friendlyName
}
checkShowImport() checkShowImport()
} }
@ -119,7 +126,7 @@
<Modal bind:this={modal}> <Modal bind:this={modal}>
<ModalContent <ModalContent
disabled={!Object.keys(integration).length} disabled={!Object.keys(integration).length}
title="Add data source" title="Add datasource"
confirmText="Continue" confirmText="Continue"
showSecondaryButton={showImportButton} showSecondaryButton={showImportButton}
secondaryButtonText="Import" secondaryButtonText="Import"
@ -148,38 +155,41 @@
</Layout> </Layout>
<Layout noPadding gap="XS"> <Layout noPadding gap="XS">
<Body size="S">Connect to an external data source</Body> <Body size="S">Connect to an external datasource</Body>
<div class="item-list"> <div class="item-list">
{#each Object.entries(integrations).filter(([key]) => key !== IntegrationTypes.INTERNAL) as [integrationType, schema]} {#each Object.entries(integrations).filter(([key, val]) => key !== IntegrationTypes.INTERNAL && !val.custom) as [integrationType, schema]}
<div <DatasourceCard
class:selected={integration.type === integrationType} on:selected={evt => selectIntegration(evt.detail)}
on:click={() => selectIntegration(integrationType)} {schema}
class="item hoverable" bind:integrationType
> {integration}
<div class="item-body" class:with-type={!!schema.type}> />
<svelte:component
this={ICONS[integrationType]}
height="20"
width="20"
/>
<div class="text">
<Heading size="XXS">{schema.friendlyName}</Heading>
{#if schema.type}
<Detail size="S">{schema.type || ""}</Detail>
{/if}
</div>
</div>
</div>
{/each} {/each}
</div> </div>
</Layout> </Layout>
{#if customIntegrations.length > 0}
<Layout noPadding gap="XS">
<Body size="S">Custom datasource</Body>
<div class="item-list">
{#each customIntegrations as [integrationType, schema]}
<DatasourceCard
on:selected={evt => selectIntegration(evt.detail)}
{schema}
bind:integrationType
{integration}
/>
{/each}
</div>
</Layout>
{/if}
</ModalContent> </ModalContent>
</Modal> </Modal>
<style> <style>
.item-list { .item-list {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); grid-template-columns: repeat(2, minmax(150px, 1fr));
grid-gap: var(--spectrum-alias-grid-baseline); grid-gap: var(--spectrum-alias-grid-baseline);
} }

View File

@ -14,8 +14,14 @@
let datasource = cloneDeep(integration) let datasource = cloneDeep(integration)
let skipFetch = false let skipFetch = false
$: name =
IntegrationNames[datasource.type] || datasource.name || datasource.type
async function saveDatasource() { async function saveDatasource() {
try { try {
if (!datasource.name) {
datasource.name = name
}
const resp = await save(datasource, skipFetch) const resp = await save(datasource, skipFetch)
$goto(`./datasource/${resp._id}`) $goto(`./datasource/${resp._id}`)
notifications.success(`Datasource updated successfully.`) notifications.success(`Datasource updated successfully.`)
@ -32,7 +38,7 @@
</script> </script>
<ModalContent <ModalContent
title={`Connect to ${IntegrationNames[datasource.type]}`} title={`Connect to ${name}`}
onConfirm={() => saveDatasource()} onConfirm={() => saveDatasource()}
onCancel={() => modal.show()} onCancel={() => modal.show()}
confirmText={datasource.plus confirmText={datasource.plus

View File

@ -10,6 +10,7 @@
} from "@budibase/bbui" } from "@budibase/bbui"
import TemplateCard from "components/common/TemplateCard.svelte" import TemplateCard from "components/common/TemplateCard.svelte"
import CreateAppModal from "components/start/CreateAppModal.svelte" import CreateAppModal from "components/start/CreateAppModal.svelte"
import { licensing } from "stores/portal"
export let templates export let templates
@ -96,15 +97,17 @@
backgroundColour={templateEntry.background} backgroundColour={templateEntry.background}
icon={templateEntry.icon} icon={templateEntry.icon}
> >
<Button {#if !($licensing?.usageMetrics?.apps >= 100)}
cta <Button
on:click={() => { cta
template = templateEntry on:click={() => {
creationModal.show() template = templateEntry
}} creationModal.show()
> }}
Use template >
</Button> Use template
</Button>
{/if}
<a <a
href={templateEntry.url} href={templateEntry.url}
target="_blank" target="_blank"

View File

@ -9,6 +9,9 @@
Body, Body,
Layout, Layout,
Button, Button,
ActionButton,
Icon,
Popover,
} from "@budibase/bbui" } from "@budibase/bbui"
import { createEventDispatcher, onMount } from "svelte" import { createEventDispatcher, onMount } from "svelte"
import { import {
@ -45,9 +48,25 @@
let jsValue = initialValueJS ? value : null let jsValue = initialValueJS ? value : null
let hbsValue = initialValueJS ? null : value let hbsValue = initialValueJS ? null : value
let selectedCategory = null
let popover
let popoverAnchor
let hoverTarget
$: usingJS = mode === "JavaScript" $: usingJS = mode === "JavaScript"
$: searchRgx = new RegExp(search, "ig") $: searchRgx = new RegExp(search, "ig")
$: categories = Object.entries(groupBy("category", bindings)) $: categories = Object.entries(groupBy("category", bindings))
$: bindingIcons = bindings?.reduce((acc, ele) => {
if (ele.icon) {
acc[ele.category] = acc[ele.category] || ele.icon
}
return acc
}, {})
$: categoryIcons = { ...bindingIcons, Helpers: "MagicWand" }
$: filteredCategories = categories $: filteredCategories = categories
.map(([name, categoryBindings]) => ({ .map(([name, categoryBindings]) => ({
name, name,
@ -55,10 +74,19 @@
return binding.readableBinding.match(searchRgx) return binding.readableBinding.match(searchRgx)
}), }),
})) }))
.filter(category => category.bindings?.length > 0) .filter(category => {
return (
category.bindings?.length > 0 &&
(!selectedCategory ? true : selectedCategory === category.name)
)
})
$: filteredHelpers = helpers?.filter(helper => { $: filteredHelpers = helpers?.filter(helper => {
return helper.label.match(searchRgx) || helper.description.match(searchRgx) return helper.label.match(searchRgx) || helper.description.match(searchRgx)
}) })
$: categoryNames = [...categories.map(cat => cat[0]), "Helpers"]
$: codeMirrorHints = bindings?.map(x => `$("${x.readableBinding}")`) $: codeMirrorHints = bindings?.map(x => `$("${x.readableBinding}")`)
const updateValue = val => { const updateValue = val => {
@ -140,58 +168,163 @@
}) })
</script> </script>
<span class="detailPopover">
<Popover
align="right-side"
bind:this={popover}
anchor={popoverAnchor}
maxWidth={300}
>
<Layout gap="S">
<div class="helper">
{#if hoverTarget.title}
<div class="helper__name">{hoverTarget.title}</div>
{/if}
{#if hoverTarget.description}
<div class="helper__description">
{@html hoverTarget.description}
</div>
{/if}
{#if hoverTarget.example}
<pre class="helper__example">{hoverTarget.example}</pre>
{/if}
</div>
</Layout>
</Popover>
</span>
<DrawerContent> <DrawerContent>
<svelte:fragment slot="sidebar"> <svelte:fragment slot="sidebar">
<div class="container"> <Layout noPadding gap="S">
<section> {#if selectedCategory}
<div>
<ActionButton
secondary
icon={"ArrowLeft"}
on:click={() => {
selectedCategory = null
}}
>
Back
</ActionButton>
</div>
{/if}
{#if !selectedCategory}
<div class="heading">Search</div> <div class="heading">Search</div>
<Search placeholder="Search" bind:value={search} /> <Search placeholder="Search" bind:value={search} />
</section> {/if}
{#each filteredCategories as category}
{#if category.bindings?.length} {#if !selectedCategory && !search}
<section> <ul class="category-list">
<div class="heading">{category.name}</div> {#each categoryNames as categoryName}
<li
on:click={() => {
selectedCategory = categoryName
}}
>
<Icon name={categoryIcons[categoryName]} />
<span class="category-name">{categoryName} </span>
<span class="category-chevron"><Icon name="ChevronRight" /></span>
</li>
{/each}
</ul>
{/if}
{#if selectedCategory || search}
{#each filteredCategories as category}
{#if category.bindings?.length}
<div class="cat-heading">
<Icon name={categoryIcons[category.name]} />{category.name}
</div>
<ul> <ul>
{#each category.bindings as binding} {#each category.bindings as binding}
<li on:click={() => addBinding(binding)}> <li
<span class="binding__label">{binding.readableBinding}</span> class="binding"
{#if binding.type} on:mouseenter={e => {
<span class="binding__type">{binding.type}</span> popoverAnchor = e.target
{/if} if (!binding.description) {
{#if binding.description} return
<br /> }
<div class="binding__description"> hoverTarget = {
{binding.description || ""} title: binding.display.name || binding.fieldSchema.name,
</div> description: binding.description,
}
popover.show()
e.stopPropagation()
}}
on:mouseleave={() => {
popover.hide()
popoverAnchor = null
hoverTarget = null
}}
on:focus={() => {}}
on:blur={() => {}}
on:click={() => addBinding(binding)}
>
<span class="binding__label">
{#if binding.display?.name}
{binding.display.name}
{:else if binding.fieldSchema?.name}
{binding.fieldSchema?.name}
{:else}
{binding.readableBinding}
{/if}
</span>
{#if binding.display?.type || binding.fieldSchema?.type}
<span class="binding__typeWrap">
<span class="binding__type">
{binding.display?.type || binding.fieldSchema?.type}
</span>
</span>
{/if} {/if}
</li> </li>
{/each} {/each}
</ul> </ul>
</section> {/if}
{/each}
{#if selectedCategory === "Helpers" || search}
{#if filteredHelpers?.length}
<div class="heading">Helpers</div>
<ul class="helpers">
{#each filteredHelpers as helper}
<li
class="binding"
on:click={() => addHelper(helper, usingJS)}
on:mouseenter={e => {
popoverAnchor = e.target
if (!helper.displayText && helper.description) {
return
}
hoverTarget = {
title: helper.displayText,
description: helper.description,
example: getHelperExample(helper, usingJS),
}
popover.show()
e.stopPropagation()
}}
on:mouseleave={() => {
popover.hide()
popoverAnchor = null
hoverTarget = null
}}
on:focus={() => {}}
on:blur={() => {}}
>
<span class="binding__label">{helper.displayText}</span>
<span class="binding__typeWrap">
<span class="binding__type">function</span>
</span>
</li>
{/each}
</ul>
{/if}
{/if} {/if}
{/each}
{#if filteredHelpers?.length}
<section>
<div class="heading">Helpers</div>
<ul>
{#each filteredHelpers as helper}
<li on:click={() => addHelper(helper, usingJS)}>
<div class="helper">
<div class="helper__name">{helper.displayText}</div>
<div class="helper__description">
{@html helper.description}
</div>
<pre class="helper__example">{getHelperExample(
helper,
usingJS
)}</pre>
</div>
</li>
{/each}
</ul>
</section>
{/if} {/if}
</div> </Layout>
</svelte:fragment> </svelte:fragment>
<div class="main"> <div class="main">
<Tabs selected={mode} on:select={onChangeMode}> <Tabs selected={mode} on:select={onChangeMode}>
@ -241,6 +374,35 @@
</DrawerContent> </DrawerContent>
<style> <style>
ul.helpers li * {
pointer-events: none;
}
ul.category-list li {
display: flex;
gap: var(--spacing-m);
align-items: center;
}
ul.category-list .category-name {
font-weight: 600;
text-transform: capitalize;
}
ul.category-list .category-chevron {
flex: 1;
text-align: right;
}
ul.category-list .category-chevron :global(div.icon),
.cat-heading :global(div.icon) {
display: inline-block;
}
li.binding {
display: flex;
align-items: center;
}
li.binding .binding__typeWrap {
flex: 1;
text-align: right;
text-transform: capitalize;
}
.main :global(textarea) { .main :global(textarea) {
min-height: 202px !important; min-height: 202px !important;
} }
@ -251,23 +413,20 @@
padding: var(--spacing-s) var(--spacing-xl); padding: var(--spacing-s) var(--spacing-xl);
} }
.container { .heading,
margin: calc(-1 * var(--spacing-xl)); .cat-heading {
}
.heading {
font-size: var(--font-size-s); font-size: var(--font-size-s);
font-weight: 600; font-weight: 600;
text-transform: uppercase; text-transform: uppercase;
color: var(--spectrum-global-color-gray-600); color: var(--spectrum-global-color-gray-600);
padding: var(--spacing-xl) 0 var(--spacing-m) 0;
} }
section { .cat-heading {
padding: 0 var(--spacing-xl) var(--spacing-xl) var(--spacing-xl); display: flex;
} gap: var(--spacing-m);
section:not(:first-child) { align-items: center;
border-top: var(--border-light);
} }
ul { ul {
list-style: none; list-style: none;
padding: 0; padding: 0;
@ -278,7 +437,7 @@
font-size: var(--font-size-s); font-size: var(--font-size-s);
padding: var(--spacing-m); padding: var(--spacing-m);
border-radius: 4px; border-radius: 4px;
border: var(--border-light); background-color: var(--spectrum-global-color-gray-200);
transition: background-color 130ms ease-in-out, color 130ms ease-in-out, transition: background-color 130ms ease-in-out, color 130ms ease-in-out,
border-color 130ms ease-in-out; border-color 130ms ease-in-out;
word-wrap: break-word; word-wrap: break-word;
@ -292,22 +451,14 @@
li:hover { li:hover {
color: var(--spectrum-global-color-gray-900); color: var(--spectrum-global-color-gray-900);
background-color: var(--spectrum-global-color-gray-50); background-color: var(--spectrum-global-color-gray-50);
border-color: var(--spectrum-global-color-gray-500);
cursor: pointer; cursor: pointer;
} }
li:hover :global(*) {
color: var(--spectrum-global-color-gray-900) !important;
}
.binding__label { .binding__label {
font-weight: 600; font-weight: 600;
text-transform: capitalize; text-transform: capitalize;
} }
.binding__description {
color: var(--spectrum-global-color-gray-700);
margin: 0.5rem 0 0 0;
white-space: normal;
}
.binding__type { .binding__type {
font-family: monospace; font-family: monospace;
background-color: var(--spectrum-global-color-gray-200); background-color: var(--spectrum-global-color-gray-200);

View File

@ -15,7 +15,6 @@
} }
return bindings?.map(binding => ({ return bindings?.map(binding => ({
...binding, ...binding,
category: "Bindable Values",
type: null, type: null,
})) }))
} }

View File

@ -73,13 +73,13 @@
<div class="root"> <div class="root">
<Body size="S"> <Body size="S">
Choose the data source that provides the row you would like to duplicate. Choose the datasource that provides the row you would like to duplicate.
<br /> <br />
You can always add or override fields manually. You can always add or override fields manually.
</Body> </Body>
<div class="params"> <div class="params">
<Label small>Data Source</Label> <Label small>Datasource</Label>
<Select <Select
bind:value={parameters.providerId} bind:value={parameters.providerId}
options={providerOptions} options={providerOptions}

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