commit
d72358e0a1
|
@ -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
|
|
@ -59,3 +59,9 @@ jobs:
|
|||
with:
|
||||
install: false
|
||||
command: yarn test:e2e:ci
|
||||
|
||||
- name: QA Core Integration Tests
|
||||
run: |
|
||||
cd qa-core
|
||||
yarn
|
||||
yarn api:test:ci
|
|
@ -63,6 +63,7 @@ typings/
|
|||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
!qa-core/.env
|
||||
!hosting/.env
|
||||
hosting/.generated-nginx.dev.conf
|
||||
hosting/proxy/.generated-nginx.prod.conf
|
||||
|
@ -102,4 +103,6 @@ packages/builder/cypress/reports
|
|||
stats.html
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
*.tsbuildinfo
|
||||
budibase-component
|
||||
budibase-datasource
|
||||
|
|
|
@ -8,4 +8,4 @@ packages/server/client
|
|||
packages/server/src/definitions/openapi.ts
|
||||
packages/builder/.routify
|
||||
packages/builder/cypress/support/queryLevelTransformerFunction.js
|
||||
packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js
|
||||
packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js
|
|
@ -65,7 +65,7 @@ Budibase is open-source - licensed as GPL v3. This should fill you with confiden
|
|||
<br /><br />
|
||||
|
||||
### 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">
|
||||
<img alt="Budibase data" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970242/Out%20of%20beta%20launch/data_n1tlhf.png">
|
||||
|
|
|
@ -124,11 +124,15 @@ spec:
|
|||
value: {{ .Values.globals.tenantFeatureFlags | quote }}
|
||||
{{ if .Values.globals.bbAdminUserEmail }}
|
||||
- name: BB_ADMIN_USER_EMAIL
|
||||
value: { { .Values.globals.bbAdminUserEmail | quote } }
|
||||
value: {{ .Values.globals.bbAdminUserEmail | quote }}
|
||||
{{ end }}
|
||||
{{ if .Values.globals.bbAdminUserPassword }}
|
||||
- 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 }}
|
||||
{{ if .Values.services.apps.nodeDebug }}
|
||||
- name: NODE_DEBUG
|
||||
|
@ -158,7 +162,10 @@ spec:
|
|||
name: bbapps
|
||||
ports:
|
||||
- containerPort: {{ .Values.services.apps.port }}
|
||||
resources: {}
|
||||
{{ with .Values.services.apps.resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 10 }}
|
||||
{{ end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
|
|
|
@ -38,7 +38,10 @@ spec:
|
|||
image: redgeoff/replicate-couchdb-cluster
|
||||
imagePullPolicy: Always
|
||||
name: couchdb-backup
|
||||
resources: {}
|
||||
{{ with .Values.services.couchdb.backup.resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 10 }}
|
||||
{{ end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
|
|
|
@ -56,7 +56,10 @@ spec:
|
|||
name: minio-service
|
||||
ports:
|
||||
- containerPort: {{ .Values.services.objectStore.port }}
|
||||
resources: {}
|
||||
{{ with .Values.services.objectStore.resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 10 }}
|
||||
{{ end }}
|
||||
volumeMounts:
|
||||
- mountPath: /data
|
||||
name: minio-data
|
||||
|
|
|
@ -30,7 +30,10 @@ spec:
|
|||
name: proxy-service
|
||||
ports:
|
||||
- containerPort: {{ .Values.services.proxy.port }}
|
||||
resources: {}
|
||||
{{ with .Values.services.proxy.resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 10 }}
|
||||
{{ end }}
|
||||
volumeMounts:
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
|
|
|
@ -35,7 +35,10 @@ spec:
|
|||
name: redis-service
|
||||
ports:
|
||||
- containerPort: {{ .Values.services.redis.port }}
|
||||
resources: {}
|
||||
{{ with .Values.services.redis.resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 10 }}
|
||||
{{ end }}
|
||||
volumeMounts:
|
||||
- mountPath: /data
|
||||
name: redis-data
|
||||
|
|
|
@ -151,7 +151,10 @@ spec:
|
|||
name: bbworker
|
||||
ports:
|
||||
- containerPort: {{ .Values.services.worker.port }}
|
||||
resources: {}
|
||||
{{ with .Values.services.worker.resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 10 }}
|
||||
{{ end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
|
|
|
@ -60,19 +60,6 @@ ingress:
|
|||
port:
|
||||
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:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
|
@ -125,16 +112,19 @@ services:
|
|||
proxy:
|
||||
port: 10000
|
||||
replicaCount: 1
|
||||
resources: {}
|
||||
|
||||
apps:
|
||||
port: 4002
|
||||
replicaCount: 1
|
||||
logLevel: info
|
||||
resources: {}
|
||||
# nodeDebug: "" # set the value of NODE_DEBUG
|
||||
|
||||
worker:
|
||||
port: 4003
|
||||
replicaCount: 1
|
||||
resources: {}
|
||||
|
||||
couchdb:
|
||||
enabled: true
|
||||
|
@ -148,6 +138,7 @@ services:
|
|||
target: ""
|
||||
# backup interval in seconds
|
||||
interval: ""
|
||||
resources: {}
|
||||
|
||||
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
|
||||
## set, choosing the default provisioner.
|
||||
storageClass: ""
|
||||
resources: {}
|
||||
|
||||
objectStore:
|
||||
minio: true
|
||||
|
@ -177,6 +169,7 @@ services:
|
|||
## If undefined (the default) or set to null, no storageClassName spec is
|
||||
## set, choosing the default provisioner.
|
||||
storageClass: ""
|
||||
resources: {}
|
||||
|
||||
# Override values in couchDB subchart
|
||||
couchdb:
|
||||
|
|
|
@ -348,7 +348,7 @@ export interface paths {
|
|||
}
|
||||
}
|
||||
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: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["tableOutput"]
|
||||
|
@ -959,7 +959,7 @@ export interface components {
|
|||
query: {
|
||||
/** @description The ID of the query. */
|
||||
_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
|
||||
/** @description The bindings which are required to perform this query. */
|
||||
parameters?: string[]
|
||||
|
@ -983,7 +983,7 @@ export interface components {
|
|||
data: {
|
||||
/** @description The ID of the query. */
|
||||
_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
|
||||
/** @description The bindings which are required to perform this query. */
|
||||
parameters?: string[]
|
||||
|
|
|
@ -11,8 +11,8 @@
|
|||
"dependencies": {
|
||||
"bulma": "^0.9.3",
|
||||
"next": "12.1.0",
|
||||
"node-fetch": "^3.2.2",
|
||||
"node-sass": "^7.0.1",
|
||||
"node-fetch": "^3.2.10",
|
||||
"sass": "^1.52.3",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-notifications-component": "^3.4.1"
|
||||
|
@ -24,4 +24,4 @@
|
|||
"eslint-config-next": "12.1.0",
|
||||
"typescript": "4.6.2"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2020,10 +2020,10 @@ node-domexception@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
|
||||
integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
|
||||
|
||||
node-fetch@^3.2.2:
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.2.tgz#16d33fbe32ca7c6ca1ca8ba5dfea1dd885c59f04"
|
||||
integrity sha512-Cwhq1JFIoon15wcIkFzubVNFE5GvXGV82pKf4knXXjvGmn7RJKcypeuqcVNZMGDZsAFWyIRya/anwAJr7TWJ7w==
|
||||
node-fetch@^3.2.10:
|
||||
version "3.2.10"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.10.tgz#e8347f94b54ae18b57c9c049ef641cef398a85c8"
|
||||
integrity sha512-MhuzNwdURnZ1Cp4XTazr69K0BTizsBroX7Zx3UgDSVcZYKF/6p0CBe4EUb/hLqmzVhl0UpYfgRljQ4yxE+iCxA==
|
||||
dependencies:
|
||||
data-uri-to-buffer "^4.0.0"
|
||||
fetch-blob "^3.1.4"
|
||||
|
|
|
@ -22,4 +22,7 @@ BUDIBASE_ENVIRONMENT=PRODUCTION
|
|||
|
||||
# An admin user can be automatically created initially if these are set
|
||||
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=
|
|
@ -25,9 +25,12 @@ services:
|
|||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||
BB_ADMIN_USER_EMAIL: ${BB_ADMIN_USER_EMAIL}
|
||||
BB_ADMIN_USER_PASSWORD: ${BB_ADMIN_USER_PASSWORD}
|
||||
PLUGINS_DIR: ${PLUGINS_DIR}
|
||||
depends_on:
|
||||
- worker-service
|
||||
- redis-service
|
||||
# volumes:
|
||||
# - /some/path/to/plugins:/plugins
|
||||
|
||||
worker-service:
|
||||
restart: unless-stopped
|
||||
|
|
|
@ -22,4 +22,7 @@ BUDIBASE_ENVIRONMENT=PRODUCTION
|
|||
|
||||
# An admin user can be automatically created initially if these are set
|
||||
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=
|
|
@ -80,6 +80,20 @@ http {
|
|||
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 / {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
|
|
@ -158,6 +158,15 @@ http {
|
|||
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 / {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
|
|
@ -66,6 +66,15 @@ server {
|
|||
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 / {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "1.3.21",
|
||||
"version": "1.3.22-alpha.4",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
"js-yaml": "^4.1.0",
|
||||
"kill-port": "^1.6.1",
|
||||
"lerna": "3.14.1",
|
||||
"madge": "^5.0.1",
|
||||
"prettier": "^2.3.1",
|
||||
"prettier-plugin-svelte": "^2.3.0",
|
||||
"rimraf": "^3.0.2",
|
||||
|
@ -25,6 +26,7 @@
|
|||
"bootstrap": "lerna bootstrap && lerna link && ./scripts/link-dependencies.sh",
|
||||
"build": "lerna run build",
|
||||
"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:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop --exact && yarn release:pro:develop",
|
||||
"release:pro": "bash scripts/pro/release.sh",
|
||||
|
@ -45,8 +47,8 @@
|
|||
"lint:eslint": "eslint packages",
|
||||
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\"",
|
||||
"lint": "yarn run lint:eslint && yarn run lint:prettier",
|
||||
"lint:fix:eslint": "eslint --fix packages",
|
||||
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\"",
|
||||
"lint:fix:eslint": "eslint --fix packages qa-core",
|
||||
"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",
|
||||
"test:e2e": "lerna run cy:test --stream",
|
||||
"test:e2e:ci": "lerna run cy:ci --stream",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/backend-core",
|
||||
"version": "1.3.21",
|
||||
"version": "1.3.22-alpha.4",
|
||||
"description": "Budibase backend core libraries used in server and worker",
|
||||
"main": "dist/src/index.js",
|
||||
"types": "dist/src/index.d.ts",
|
||||
|
@ -20,11 +20,12 @@
|
|||
"test:watch": "jest --watchAll"
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/types": "^1.3.21",
|
||||
"@budibase/types": "1.3.22-alpha.4",
|
||||
"@shopify/jest-koa-mocks": "5.0.1",
|
||||
"@techpass/passport-openidconnect": "0.3.2",
|
||||
"aws-sdk": "2.1030.0",
|
||||
"bcrypt": "5.0.1",
|
||||
"bcryptjs": "2.4.3",
|
||||
"dotenv": "16.0.1",
|
||||
"emitter-listener": "1.1.2",
|
||||
"ioredis": "4.28.0",
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
...require("./src/plugin"),
|
||||
}
|
|
@ -7,6 +7,7 @@ exports.Cookies = {
|
|||
CurrentApp: "budibase:currentapp",
|
||||
Auth: "budibase:auth",
|
||||
Init: "budibase:init",
|
||||
ACCOUNT_RETURN_URL: "budibase:account:returnurl",
|
||||
DatasourceAuth: "budibase:datasourceauth",
|
||||
OIDC_CONFIG: "budibase:oidc:config",
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import env from "../environment"
|
|||
import { SEPARATOR, DocumentType } from "../db/constants"
|
||||
import cls from "./FunctionContext"
|
||||
import { dangerousGetDB, closeDB } from "../db"
|
||||
import { baseGlobalDBName } from "../tenancy/utils"
|
||||
import { baseGlobalDBName } from "../db/tenancy"
|
||||
import { IdentityContext } from "@budibase/types"
|
||||
import { DEFAULT_TENANT_ID as _DEFAULT_TENANT_ID } from "../constants"
|
||||
import { ContextKey } from "./constants"
|
||||
|
|
|
@ -44,6 +44,7 @@ export enum DocumentType {
|
|||
DEV_INFO = "devinfo",
|
||||
AUTOMATION_LOG = "log_au",
|
||||
ACCOUNT_METADATA = "acc_metadata",
|
||||
PLUGIN = "plg",
|
||||
}
|
||||
|
||||
export const StaticDatabases = {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { DEFAULT_TENANT_ID } from "../constants"
|
||||
import { StaticDatabases, SEPARATOR } from "../db/constants"
|
||||
import { StaticDatabases, SEPARATOR } from "./constants"
|
||||
import { getTenantId } from "../context"
|
||||
|
||||
export const getGlobalDBName = (tenantId?: string) => {
|
|
@ -3,7 +3,7 @@ import { DEFAULT_TENANT_ID, Configs } from "../constants"
|
|||
import env from "../environment"
|
||||
import { SEPARATOR, DocumentType, UNICODE_MAX, ViewName } from "./constants"
|
||||
import { getTenantId, getGlobalDB } from "../context"
|
||||
import { getGlobalDBName } from "../tenancy/utils"
|
||||
import { getGlobalDBName } from "./tenancy"
|
||||
import fetch from "node-fetch"
|
||||
import { doWithDB, allDbs } from "./index"
|
||||
import { getCouchInfo } from "./pouch"
|
||||
|
@ -16,6 +16,7 @@ import * as events from "../events"
|
|||
export * from "./constants"
|
||||
export * from "./conversions"
|
||||
export { default as Replication } from "./Replication"
|
||||
export * from "./tenancy"
|
||||
|
||||
/**
|
||||
* Generates a new app ID.
|
||||
|
@ -367,6 +368,21 @@ export const generateDevInfoID = (userId: any) => {
|
|||
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.
|
||||
* @param {Object} db - db instance to query
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { DocumentType, ViewName, DeprecatedViews, SEPARATOR } from "./utils"
|
||||
import { getGlobalDB } from "../context"
|
||||
import PouchDB from "pouchdb"
|
||||
import { StaticDatabases } from "./constants"
|
||||
import { doWithDB } from "./"
|
||||
|
||||
|
@ -201,13 +202,13 @@ export const queryView = async <T>(
|
|||
try {
|
||||
let response = await db.query<T, T>(`database/${viewName}`, params)
|
||||
const rows = response.rows
|
||||
const docs = rows.map((resp: any) =>
|
||||
params.include_docs ? resp.doc : resp.value
|
||||
)
|
||||
const docs = rows.map(row => (params.include_docs ? row.doc : row.value))
|
||||
|
||||
// if arrayResponse has been requested, always return array regardless of length
|
||||
if (opts?.arrayResponse) {
|
||||
return docs
|
||||
} else {
|
||||
// return the single document if there is only one
|
||||
return docs.length <= 1 ? docs[0] : docs
|
||||
}
|
||||
} catch (err: any) {
|
||||
|
|
|
@ -19,6 +19,7 @@ if (!LOADED && isDev() && !isTest()) {
|
|||
const env = {
|
||||
isTest,
|
||||
isDev,
|
||||
JS_BCRYPT: process.env.JS_BCRYPT,
|
||||
JWT_SECRET: process.env.JWT_SECRET,
|
||||
COUCH_DB_URL: process.env.COUCH_DB_URL || "http://localhost:4005",
|
||||
COUCH_DB_USERNAME: process.env.COUCH_DB_USER,
|
||||
|
@ -36,7 +37,7 @@ const env = {
|
|||
MULTI_TENANCY: process.env.MULTI_TENANCY,
|
||||
ACCOUNT_PORTAL_URL:
|
||||
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,
|
||||
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED || ""),
|
||||
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
|
||||
|
@ -50,6 +51,7 @@ const env = {
|
|||
GLOBAL_BUCKET_NAME: process.env.GLOBAL_BUCKET_NAME || "global",
|
||||
GLOBAL_CLOUD_BUCKET_NAME:
|
||||
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,
|
||||
DISABLE_DEVELOPER_LICENSE: process.env.DISABLE_DEVELOPER_LICENSE,
|
||||
DEFAULT_LICENSE: process.env.DEFAULT_LICENSE,
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
class BudibaseError extends Error {
|
||||
constructor(message, code, type) {
|
||||
super(message)
|
||||
this.code = code
|
||||
this.type = type
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
BudibaseError,
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
const http = require("./http")
|
||||
const licensing = require("./licensing")
|
||||
import { HTTPError } from "./http"
|
||||
import { UsageLimitError, FeatureDisabledError } from "./licensing"
|
||||
import * as licensing from "./licensing"
|
||||
|
||||
const codes = {
|
||||
...licensing.codes,
|
||||
|
@ -11,7 +12,7 @@ const context = {
|
|||
...licensing.context,
|
||||
}
|
||||
|
||||
const getPublicError = err => {
|
||||
const getPublicError = (err: any) => {
|
||||
let error
|
||||
if (err.code || err.type) {
|
||||
// add generic error information
|
||||
|
@ -32,13 +33,15 @@ const getPublicError = err => {
|
|||
return error
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
const pkg = {
|
||||
codes,
|
||||
types,
|
||||
errors: {
|
||||
UsageLimitError: licensing.UsageLimitError,
|
||||
FeatureDisabledError: licensing.FeatureDisabledError,
|
||||
HTTPError: http.HTTPError,
|
||||
UsageLimitError,
|
||||
FeatureDisabledError,
|
||||
HTTPError,
|
||||
},
|
||||
getPublicError,
|
||||
}
|
||||
|
||||
export = pkg
|
|
@ -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,
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -5,8 +5,15 @@ import {
|
|||
DatasourceCreatedEvent,
|
||||
DatasourceUpdatedEvent,
|
||||
DatasourceDeletedEvent,
|
||||
SourceName,
|
||||
} 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(
|
||||
datasource: Datasource,
|
||||
timestamp?: string | number
|
||||
|
@ -14,6 +21,7 @@ export async function created(
|
|||
const properties: DatasourceCreatedEvent = {
|
||||
datasourceId: datasource._id as string,
|
||||
source: datasource.source,
|
||||
custom: isCustom(datasource),
|
||||
}
|
||||
await publishEvent(Event.DATASOURCE_CREATED, properties, timestamp)
|
||||
}
|
||||
|
@ -22,6 +30,7 @@ export async function updated(datasource: Datasource) {
|
|||
const properties: DatasourceUpdatedEvent = {
|
||||
datasourceId: datasource._id as string,
|
||||
source: datasource.source,
|
||||
custom: isCustom(datasource),
|
||||
}
|
||||
await publishEvent(Event.DATASOURCE_UPDATED, properties)
|
||||
}
|
||||
|
@ -30,6 +39,7 @@ export async function deleted(datasource: Datasource) {
|
|||
const properties: DatasourceDeletedEvent = {
|
||||
datasourceId: datasource._id as string,
|
||||
source: datasource.source,
|
||||
custom: isCustom(datasource),
|
||||
}
|
||||
await publishEvent(Event.DATASOURCE_DELETED, properties)
|
||||
}
|
||||
|
|
|
@ -18,3 +18,4 @@ export * as view from "./view"
|
|||
export * as installation from "./installation"
|
||||
export * as backfill from "./backfill"
|
||||
export * as group from "./group"
|
||||
export * as plugin from "./plugin"
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -31,20 +31,26 @@ const TENANT_FEATURE_FLAGS = getFeatureFlags()
|
|||
|
||||
exports.isEnabled = featureFlag => {
|
||||
const tenantId = tenancy.getTenantId()
|
||||
|
||||
return (
|
||||
TENANT_FEATURE_FLAGS &&
|
||||
TENANT_FEATURE_FLAGS[tenantId] &&
|
||||
TENANT_FEATURE_FLAGS[tenantId].includes(featureFlag)
|
||||
)
|
||||
const flags = exports.getTenantFeatureFlags(tenantId)
|
||||
return flags.includes(featureFlag)
|
||||
}
|
||||
|
||||
exports.getTenantFeatureFlags = tenantId => {
|
||||
if (TENANT_FEATURE_FLAGS && TENANT_FEATURE_FLAGS[tenantId]) {
|
||||
return TENANT_FEATURE_FLAGS[tenantId]
|
||||
const flags = []
|
||||
|
||||
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 = {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
const bcrypt = require("bcrypt")
|
||||
const env = require("./environment")
|
||||
const bcrypt = env.JS_BCRYPT ? require("bcryptjs") : require("bcrypt")
|
||||
const { v4 } = require("uuid")
|
||||
|
||||
const SALT_ROUNDS = env.SALT_ROUNDS || 10
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import errors from "./errors"
|
||||
|
||||
const errorClasses = errors.errors
|
||||
import * as events from "./events"
|
||||
import * as migrations from "./migrations"
|
||||
|
@ -15,9 +14,10 @@ import deprovisioning from "./context/deprovision"
|
|||
import auth from "./auth"
|
||||
import constants from "./constants"
|
||||
import * as dbConstants from "./db/constants"
|
||||
import logging from "./logging"
|
||||
import * as logging from "./logging"
|
||||
import pino from "./pino"
|
||||
import * as middleware from "./middleware"
|
||||
import plugins from "./plugin"
|
||||
|
||||
// mimic the outer package exports
|
||||
import * as db from "./pkg/db"
|
||||
|
@ -56,6 +56,7 @@ const core = {
|
|||
errors,
|
||||
logging,
|
||||
roles,
|
||||
plugins,
|
||||
...pino,
|
||||
...errorClasses,
|
||||
middleware,
|
||||
|
|
|
@ -106,6 +106,7 @@ export = (
|
|||
user = await getUser(userId, session.tenantId)
|
||||
}
|
||||
user.csrfToken = session.csrfToken
|
||||
|
||||
if (session?.lastAccessedAt < timeMinusOneMinute()) {
|
||||
// make sure we denote that the session is still in use
|
||||
await updateSessionTTL(session)
|
||||
|
|
|
@ -17,14 +17,6 @@ export const DEFINITIONS: MigrationDefinition[] = [
|
|||
type: MigrationType.APP,
|
||||
name: MigrationName.APP_URLS,
|
||||
},
|
||||
{
|
||||
type: MigrationType.GLOBAL,
|
||||
name: MigrationName.DEVELOPER_QUOTA,
|
||||
},
|
||||
{
|
||||
type: MigrationType.GLOBAL,
|
||||
name: MigrationName.PUBLISHED_APP_QUOTA,
|
||||
},
|
||||
{
|
||||
type: MigrationType.APP,
|
||||
name: MigrationName.EVENT_APP_BACKFILL,
|
||||
|
|
|
@ -3,12 +3,8 @@ import { doWithDB } from "../db"
|
|||
import { DocumentType, StaticDatabases } from "../db/constants"
|
||||
import { getAllApps } from "../db/utils"
|
||||
import environment from "../environment"
|
||||
import {
|
||||
doInTenant,
|
||||
getTenantIds,
|
||||
getGlobalDBName,
|
||||
getTenantId,
|
||||
} from "../tenancy"
|
||||
import { doInTenant, getTenantIds, getTenantId } from "../tenancy"
|
||||
import { getGlobalDBName } from "../db/tenancy"
|
||||
import * as context from "../context"
|
||||
import { DEFINITIONS } from "."
|
||||
import {
|
||||
|
|
|
@ -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.
|
||||
|
@ -172,6 +176,14 @@ export const streamUpload = async (
|
|||
const objectStore = ObjectStore(bucketName)
|
||||
await makeSureBucketExists(objectStore, bucketName)
|
||||
|
||||
// Set content type for certain known extensions
|
||||
if (filename?.endsWith(".js")) {
|
||||
extra = {
|
||||
...extra,
|
||||
ContentType: "application/javascript",
|
||||
}
|
||||
}
|
||||
|
||||
const params = {
|
||||
Bucket: sanitizeBucket(bucketName),
|
||||
Key: sanitizeKey(filename),
|
||||
|
@ -295,9 +307,13 @@ export const uploadDirectory = async (
|
|||
return files
|
||||
}
|
||||
|
||||
exports.downloadTarballDirect = async (url: string, path: string) => {
|
||||
exports.downloadTarballDirect = async (
|
||||
url: string,
|
||||
path: string,
|
||||
headers = {}
|
||||
) => {
|
||||
path = sanitizeKey(path)
|
||||
const response = await fetch(url)
|
||||
const response = await fetch(url, { headers })
|
||||
if (!response.ok) {
|
||||
throw new Error(`unexpected response ${response.statusText}`)
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ exports.ObjectStoreBuckets = {
|
|||
TEMPLATES: env.TEMPLATES_BUCKET_NAME,
|
||||
GLOBAL: env.GLOBAL_BUCKET_NAME,
|
||||
GLOBAL_CLOUD: env.GLOBAL_CLOUD_BUCKET_NAME,
|
||||
PLUGINS: env.PLUGIN_BUCKET_NAME,
|
||||
}
|
||||
|
||||
exports.budibaseTempDir = function () {
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import * as utils from "./utils"
|
||||
|
||||
const pkg = {
|
||||
...utils,
|
||||
}
|
||||
|
||||
export = pkg
|
|
@ -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}`)
|
||||
}
|
||||
}
|
|
@ -2,28 +2,12 @@ const redis = require("../redis/init")
|
|||
const { v4: uuidv4 } = require("uuid")
|
||||
const { logWarn } = require("../logging")
|
||||
const env = require("../environment")
|
||||
|
||||
interface CreateSession {
|
||||
sessionId: string
|
||||
tenantId: string
|
||||
csrfToken?: string
|
||||
}
|
||||
|
||||
interface Session extends CreateSession {
|
||||
userId: string
|
||||
lastAccessedAt: string
|
||||
createdAt: string
|
||||
// make optional attributes required
|
||||
csrfToken: string
|
||||
}
|
||||
|
||||
interface SessionKey {
|
||||
key: string
|
||||
}
|
||||
|
||||
interface ScannedSession {
|
||||
value: Session
|
||||
}
|
||||
import {
|
||||
Session,
|
||||
ScannedSession,
|
||||
SessionKey,
|
||||
CreateSession,
|
||||
} from "@budibase/types"
|
||||
|
||||
// a week in seconds
|
||||
const EXPIRY_SECONDS = 86400 * 7
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import * as context from "../context"
|
||||
import * as tenancy from "./tenancy"
|
||||
import * as utils from "./utils"
|
||||
|
||||
const pkg = {
|
||||
...context,
|
||||
...tenancy,
|
||||
...utils,
|
||||
}
|
||||
|
||||
export = pkg
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { doWithDB } from "../db"
|
||||
import { queryPlatformView } from "../db/views"
|
||||
import { StaticDatabases, ViewName } from "../db/constants"
|
||||
import { getGlobalDBName } from "./utils"
|
||||
import { getGlobalDBName } from "../db/tenancy"
|
||||
import {
|
||||
getTenantId,
|
||||
DEFAULT_TENANT_ID,
|
||||
|
@ -9,7 +9,7 @@ import {
|
|||
getTenantIDFromAppID,
|
||||
} from "../context"
|
||||
import env from "../environment"
|
||||
import { PlatformUser, PlatformUserByEmail } from "@budibase/types"
|
||||
import { PlatformUser } from "@budibase/types"
|
||||
|
||||
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants
|
||||
const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name
|
||||
|
|
|
@ -1,29 +1,39 @@
|
|||
const {
|
||||
import {
|
||||
ViewName,
|
||||
getUsersByAppParams,
|
||||
getProdAppID,
|
||||
generateAppUserID,
|
||||
} = require("./db/utils")
|
||||
const { queryGlobalView } = require("./db/views")
|
||||
const { UNICODE_MAX } = require("./db/constants")
|
||||
} from "./db/utils"
|
||||
import { queryGlobalView } from "./db/views"
|
||||
import { UNICODE_MAX } from "./db/constants"
|
||||
import { User } from "@budibase/types"
|
||||
|
||||
/**
|
||||
* Given an email address this will use a view to search through
|
||||
* all the users to find one with this email address.
|
||||
* @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) {
|
||||
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(),
|
||||
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") {
|
||||
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]
|
||||
}
|
||||
|
||||
exports.getGlobalUserByAppPage = (appId, user) => {
|
||||
export const getGlobalUserByAppPage = (appId: string, user: User) => {
|
||||
if (!user) {
|
||||
return
|
||||
}
|
||||
return generateAppUserID(getProdAppID(appId), user._id)
|
||||
return generateAppUserID(getProdAppID(appId), user._id!)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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") {
|
||||
throw new Error("Must provide a string to search by")
|
||||
}
|
||||
const lcEmail = email.toLowerCase()
|
||||
// handle if passing up startkey for pagination
|
||||
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,
|
||||
startkey,
|
||||
endkey: `${lcEmail}${UNICODE_MAX}`,
|
|
@ -42,6 +42,18 @@ async function resolveAppUrl(ctx) {
|
|||
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
|
||||
* @param {object} ctx The main request body to look through.
|
||||
|
|
|
@ -1377,6 +1377,11 @@ bcrypt@5.0.1:
|
|||
"@mapbox/node-pre-gyp" "^1.0.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:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/bbui",
|
||||
"description": "A UI solution used in the different Budibase projects.",
|
||||
"version": "1.3.21",
|
||||
"version": "1.3.22-alpha.4",
|
||||
"license": "MPL-2.0",
|
||||
"svelte": "src/index.js",
|
||||
"module": "dist/bbui.es.js",
|
||||
|
@ -38,7 +38,7 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
|
||||
"@budibase/string-templates": "^1.3.21",
|
||||
"@budibase/string-templates": "1.3.22-alpha.4",
|
||||
"@spectrum-css/actionbutton": "^1.0.1",
|
||||
"@spectrum-css/actiongroup": "^1.0.1",
|
||||
"@spectrum-css/avatar": "^3.0.2",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export default function positionDropdown(element, { anchor, align }) {
|
||||
export default function positionDropdown(element, { anchor, align, maxWidth }) {
|
||||
let positionSide = "top"
|
||||
let maxHeight = 0
|
||||
let dimensions = getDimensions(anchor)
|
||||
|
@ -34,13 +34,24 @@ export default function positionDropdown(element, { anchor, align }) {
|
|||
}
|
||||
|
||||
function calcLeftPosition() {
|
||||
return align === "right"
|
||||
? dimensions.left + dimensions.width - dimensions.containerWidth
|
||||
: dimensions.left
|
||||
let 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.zIndex = "9999"
|
||||
if (maxWidth) {
|
||||
element.style.maxWidth = `${maxWidth}px`
|
||||
}
|
||||
element.style.minWidth = `${dimensions.width}px`
|
||||
element.style.maxHeight = `${maxHeight.toFixed(0)}px`
|
||||
element.style.transformOrigin = `center ${positionSide}`
|
||||
|
@ -54,10 +65,8 @@ export default function positionDropdown(element, { anchor, align }) {
|
|||
element.style.left = `${calcLeftPosition(dimensions).toFixed(0)}px`
|
||||
})
|
||||
})
|
||||
|
||||
resizeObserver.observe(anchor)
|
||||
resizeObserver.observe(element)
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
resizeObserver.disconnect()
|
||||
|
|
|
@ -4,22 +4,32 @@
|
|||
import { banner } from "../Stores/banner"
|
||||
import Banner from "./Banner.svelte"
|
||||
import { fly } from "svelte/transition"
|
||||
import TooltipWrapper from "../Tooltip/TooltipWrapper.svelte"
|
||||
</script>
|
||||
|
||||
<Portal target=".banner-container">
|
||||
<div class="banner">
|
||||
{#if $banner.message}
|
||||
{#each $banner.messages as message}
|
||||
<div transition:fly={{ y: -30 }}>
|
||||
<Banner
|
||||
type={$banner.type}
|
||||
extraButtonText={$banner.extraButtonText}
|
||||
extraButtonAction={$banner.extraButtonAction}
|
||||
on:change={$banner.onChange}
|
||||
type={message.type}
|
||||
extraButtonText={message.extraButtonText}
|
||||
extraButtonAction={message.extraButtonAction}
|
||||
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>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</Portal>
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
export let header = ""
|
||||
export let message = ""
|
||||
export let onConfirm = undefined
|
||||
export let buttonText = ""
|
||||
|
||||
$: icon = selectIcon(type)
|
||||
// if newlines used, convert them to different elements
|
||||
|
@ -39,13 +40,16 @@
|
|||
<div class="spectrum-InLineAlert-content">{splitMsg}</div>
|
||||
{/each}
|
||||
{#if onConfirm}
|
||||
<div class="spectrum-InLineAlert-footer">
|
||||
<Button secondary on:click={onConfirm}>OK</Button>
|
||||
<div class="spectrum-InLineAlert-footer button">
|
||||
<Button secondary on:click={onConfirm}>{buttonText || "OK"}</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.spectrum-InLineAlert {
|
||||
--spectrum-semantic-negative-border-color: #e34850;
|
||||
--spectrum-semantic-positive-border-color: #2d9d78;
|
||||
|
|
|
@ -4,10 +4,15 @@
|
|||
|
||||
export let size = "M"
|
||||
export let tooltip = ""
|
||||
export let muted
|
||||
</script>
|
||||
|
||||
<TooltipWrapper {tooltip} {size}>
|
||||
<label for="" class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`}>
|
||||
<label
|
||||
class:muted
|
||||
for=""
|
||||
class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`}
|
||||
>
|
||||
<slot />
|
||||
</label>
|
||||
</TooltipWrapper>
|
||||
|
@ -17,4 +22,8 @@
|
|||
padding: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.muted {
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -24,7 +24,6 @@
|
|||
export let secondaryAction = undefined
|
||||
export let secondaryButtonWarning = false
|
||||
export let dataCy = null
|
||||
|
||||
const { hide, cancel } = getContext(Context.Modal)
|
||||
let loading = false
|
||||
$: confirmDisabled = disabled || loading
|
||||
|
@ -88,12 +87,11 @@
|
|||
<section class="spectrum-Dialog-content content-grid">
|
||||
<slot />
|
||||
</section>
|
||||
{#if showCancelButton || showConfirmButton}
|
||||
{#if showCancelButton || showConfirmButton || $$slots.footer}
|
||||
<div
|
||||
class="spectrum-ButtonGroup spectrum-Dialog-buttonGroup spectrum-Dialog-buttonGroup--noFooter"
|
||||
>
|
||||
<slot name="footer" />
|
||||
|
||||
{#if showSecondaryButton && secondaryButtonText && secondaryAction}
|
||||
<div class="secondary-action">
|
||||
<Button
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
export let align = "right"
|
||||
export let portalTarget
|
||||
export let dataCy
|
||||
export let maxWidth
|
||||
|
||||
export let direction = "bottom"
|
||||
export let showTip = false
|
||||
|
@ -45,7 +46,7 @@
|
|||
<Portal target={portalTarget}>
|
||||
<div
|
||||
tabindex="0"
|
||||
use:positionDropdown={{ anchor, align }}
|
||||
use:positionDropdown={{ anchor, align, maxWidth }}
|
||||
use:clickOutside={hide}
|
||||
on:keydown={handleEscape}
|
||||
class={"spectrum-Popover is-open " + (tooltipClasses || "")}
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
export let duration = 1000
|
||||
export let width = false
|
||||
export let sideLabel = false
|
||||
export let hidePercentage = true
|
||||
export let color // red, green, default = blue
|
||||
|
||||
export let size = "M"
|
||||
|
||||
|
@ -37,7 +39,7 @@
|
|||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
{#if value || value === 0}
|
||||
{#if !hidePercentage && (value || value === 0)}
|
||||
<div
|
||||
class="spectrum-FieldLabel spectrum-ProgressBar-percentage spectrum-FieldLabel--size{size}"
|
||||
>
|
||||
|
@ -47,8 +49,19 @@
|
|||
<div class="spectrum-ProgressBar-track">
|
||||
<div
|
||||
class="spectrum-ProgressBar-fill"
|
||||
class:color-green={color === "green"}
|
||||
class:color-red={color === "red"}
|
||||
style={value || value === 0 ? `width: ${$progress}%` : ""}
|
||||
/>
|
||||
</div>
|
||||
<div class="spectrum-ProgressBar-label" hidden="" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.color-green {
|
||||
background: #009562;
|
||||
}
|
||||
.color-red {
|
||||
background: #dd2019;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
const multilevel = getContext("sidenav-type")
|
||||
import Badge from "../Badge/Badge.svelte"
|
||||
export let href = ""
|
||||
export let external = false
|
||||
export let heading = ""
|
||||
|
@ -8,6 +9,7 @@
|
|||
export let selected = false
|
||||
export let disabled = false
|
||||
export let dataCy
|
||||
export let badge = ""
|
||||
</script>
|
||||
|
||||
<li
|
||||
|
@ -38,10 +40,22 @@
|
|||
</svg>
|
||||
{/if}
|
||||
<slot />
|
||||
{#if badge}
|
||||
<div class="badge">
|
||||
<Badge active size="S">{badge}</Badge>
|
||||
</div>
|
||||
{/if}
|
||||
</a>
|
||||
|
||||
{#if multilevel && $$slots.subnav}
|
||||
<ul class="spectrum-SideNav">
|
||||
<slot name="subnav" />
|
||||
</ul>
|
||||
{/if}
|
||||
</li>
|
||||
|
||||
<style>
|
||||
.badge {
|
||||
margin-left: 10px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
import { writable } from "svelte/store"
|
||||
|
||||
export const BANNER_TYPES = {
|
||||
INFO: "info",
|
||||
NEGATIVE: "negative",
|
||||
}
|
||||
|
||||
export function createBannerStore() {
|
||||
const DEFAULT_CONFIG = {}
|
||||
const DEFAULT_CONFIG = {
|
||||
messages: [],
|
||||
}
|
||||
|
||||
const banner = writable(DEFAULT_CONFIG)
|
||||
|
||||
|
@ -20,17 +27,38 @@ export function createBannerStore() {
|
|||
const showStatus = async () => {
|
||||
const config = {
|
||||
message: "Some systems are experiencing issues",
|
||||
type: "negative",
|
||||
type: BANNER_TYPES.NEGATIVE,
|
||||
extraButtonText: "View Status",
|
||||
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 {
|
||||
subscribe: banner.subscribe,
|
||||
showStatus,
|
||||
show,
|
||||
queue,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
export let tooltip = ""
|
||||
export let size = "M"
|
||||
export let disabled = true
|
||||
|
||||
let showTooltip = false
|
||||
</script>
|
||||
|
@ -19,7 +20,7 @@
|
|||
on:mouseleave={() => (showTooltip = false)}
|
||||
on:focus
|
||||
>
|
||||
<Icon name="InfoOutline" size="S" disabled={true} />
|
||||
<Icon name="InfoOutline" size="S" {disabled} />
|
||||
</div>
|
||||
{#if showTooltip}
|
||||
<div class="tooltip">
|
||||
|
@ -54,7 +55,6 @@
|
|||
transform: scale(0.75);
|
||||
}
|
||||
.icon-small {
|
||||
margin-top: -2px;
|
||||
margin-bottom: -5px;
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -34,6 +34,7 @@ export { default as Layout } from "./Layout/Layout.svelte"
|
|||
export { default as Page } from "./Layout/Page.svelte"
|
||||
export { default as Link } from "./Link/Link.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 MenuSection } from "./Menu/Section.svelte"
|
||||
export { default as MenuSeparator } from "./Menu/Separator.svelte"
|
||||
|
@ -94,7 +95,7 @@ export { default as clickOutside } from "./Actions/click_outside"
|
|||
|
||||
// Stores
|
||||
export { notifications, createNotificationStore } from "./Stores/notifications"
|
||||
export { banner } from "./Stores/banner"
|
||||
export { banner, BANNER_TYPES } from "./Stores/banner"
|
||||
|
||||
// Helpers
|
||||
export * as Helpers from "./helpers"
|
||||
|
|
|
@ -74,11 +74,11 @@ filterTests(["smoke", "all"], () => {
|
|||
.contains("Update role")
|
||||
.click({ force: true })
|
||||
})
|
||||
cy.reload({ timeout: 5000 })
|
||||
cy.reload()
|
||||
cy.wait(1000)
|
||||
}
|
||||
// Confirm roles exist within Configure roles table
|
||||
cy.get(interact.SPECTRUM_TABLE, { timeout: 2000 })
|
||||
cy.get(interact.SPECTRUM_TABLE, { timeout: 20000 })
|
||||
.eq(0)
|
||||
.within(assginedRoles => {
|
||||
expect(assginedRoles).to.contain("Admin")
|
||||
|
@ -180,7 +180,7 @@ filterTests(["smoke", "all"], () => {
|
|||
cy.reload()
|
||||
|
||||
// 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.FIELD, { timeout: 1000 }).eq(2).within(() => {
|
||||
|
|
|
@ -82,10 +82,10 @@ filterTests(['smoke', 'all'], () => {
|
|||
})
|
||||
|
||||
if (Cypress.env("TEST_ENV")) {
|
||||
it("should generate data source screens", () => {
|
||||
// Using MySQL data source for testing this
|
||||
it("should generate datasource screens", () => {
|
||||
// Using MySQL datasource for testing this
|
||||
const datasource = "MySQL"
|
||||
// Select & configure MySQL data source
|
||||
// Select & configure MySQL datasource
|
||||
cy.selectExternalDatasource(datasource)
|
||||
cy.addDatasourceConfig(datasource)
|
||||
// Create Autogenerated screens from a MySQL table - MySQL contains books table
|
||||
|
|
|
@ -10,7 +10,7 @@ filterTests(['smoke', 'all'], () => {
|
|||
|
||||
it("should add a current user binding", () => {
|
||||
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"
|
||||
cy.createScreen(`/test/:${paramName}`)
|
||||
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
|
||||
// is check that we were able to bind to the property, and that the
|
||||
// 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(".category-list li").contains(bindingCategories[0])
|
||||
cy.get(".drawer").within(() => {
|
||||
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} }}`)
|
||||
} else {
|
||||
cy.get("textarea").type(bindingText)
|
||||
|
|
|
@ -2,7 +2,7 @@ import filterTests from "../support/filterTests"
|
|||
const interact = require("../support/interact")
|
||||
|
||||
filterTests(["all"], () => {
|
||||
context("Create Components", () => {
|
||||
xcontext("Create Components", () => {
|
||||
let headlineId
|
||||
|
||||
before(() => {
|
||||
|
@ -20,7 +20,7 @@ filterTests(["all"], () => {
|
|||
//Use the tree to delete a selected component
|
||||
const deleteSelectedComponent = () => {
|
||||
cy.get(
|
||||
".nav-items-container .nav-item.selected .actions > div > .icon"
|
||||
".nav-item.selected .actions > div > .icon"
|
||||
).click({
|
||||
force: true,
|
||||
})
|
||||
|
@ -91,7 +91,7 @@ filterTests(["all"], () => {
|
|||
cy.searchAndAddComponent("Paragraph").then(componentId => {
|
||||
cy.get("[data-cy=setting-_instanceName] input").type(componentId).blur()
|
||||
cy.get(
|
||||
".nav-items-container .nav-item.selected .actions > div > .icon"
|
||||
".nav-item.selected .actions > div > .icon"
|
||||
).click({
|
||||
force: true,
|
||||
})
|
||||
|
@ -145,7 +145,7 @@ filterTests(["all"], () => {
|
|||
return testFieldFocusOnCreate(label)
|
||||
})
|
||||
.then(() => {
|
||||
cy.get(".nav-items-container .nav-item")
|
||||
cy.get(".nav-item")
|
||||
.contains(formId)
|
||||
.click({ force: true })
|
||||
deleteSelectedComponent()
|
||||
|
@ -195,7 +195,7 @@ filterTests(["all"], () => {
|
|||
return testFocusOnCreate(label)
|
||||
})
|
||||
.then(() => {
|
||||
cy.get(".nav-items-container .nav-item")
|
||||
cy.get(".nav-item")
|
||||
.contains(providerId)
|
||||
.click({ force: true })
|
||||
deleteSelectedComponent()
|
||||
|
@ -218,7 +218,7 @@ filterTests(["all"], () => {
|
|||
.find(".component-placeholder")
|
||||
.should("not.exist")
|
||||
cy.getComponent(imageId).find(`img[alt=${imageId}]`).should("exist")
|
||||
cy.get(".nav-items-container .nav-item")
|
||||
cy.get(".nav-item")
|
||||
.contains(imageId)
|
||||
.click({ force: true })
|
||||
deleteSelectedComponent()
|
||||
|
@ -242,7 +242,7 @@ filterTests(["all"], () => {
|
|||
cy.getComponent(markdownId)
|
||||
.find(".editor-preview-full h1")
|
||||
.contains("Hi")
|
||||
cy.get(".nav-items-container .nav-item")
|
||||
cy.get(".nav-item")
|
||||
.contains(markdownId)
|
||||
.click({ force: true })
|
||||
deleteSelectedComponent()
|
||||
|
@ -265,7 +265,7 @@ filterTests(["all"], () => {
|
|||
.find(".component-placeholder")
|
||||
.should("not.exist")
|
||||
cy.getComponent(iconId).find("i.ri-save-fill").should("exist")
|
||||
cy.get(".nav-items-container .nav-item")
|
||||
cy.get(".nav-item")
|
||||
.contains(iconId)
|
||||
.click({ force: true })
|
||||
deleteSelectedComponent()
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import filterTests from "../../support/filterTests"
|
||||
|
||||
filterTests(['all'], () => {
|
||||
context("Datasource Wizard", () => {
|
||||
xcontext("Datasource Wizard", () => {
|
||||
if (Cypress.env("TEST_ENV")) {
|
||||
before(() => {
|
||||
cy.login()
|
||||
|
|
|
@ -11,8 +11,8 @@ filterTests(["all"], () => {
|
|||
const queryName = "Cypress Test Query"
|
||||
const queryRename = "CT Query Rename"
|
||||
|
||||
it("Should add MySQL data source without configuration", () => {
|
||||
// Select MySQL data source
|
||||
it("Should add MySQL datasource without configuration", () => {
|
||||
// Select MySQL datasource
|
||||
cy.selectExternalDatasource(datasource)
|
||||
// Attempt to fetch tables without applying configuration
|
||||
cy.intercept("**/datasources").as("datasource")
|
||||
|
@ -35,8 +35,8 @@ filterTests(["all"], () => {
|
|||
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true })
|
||||
})
|
||||
|
||||
it("should add MySQL data source and fetch tables", () => {
|
||||
// Add & configure MySQL data source
|
||||
it("should add MySQL datasource and fetch tables", () => {
|
||||
// Add & configure MySQL datasource
|
||||
cy.selectExternalDatasource(datasource)
|
||||
cy.intercept("**/datasources").as("datasource")
|
||||
cy.addDatasourceConfig(datasource)
|
||||
|
@ -52,7 +52,7 @@ filterTests(["all"], () => {
|
|||
})
|
||||
|
||||
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")
|
||||
.should("contain", "Error fetching tables")
|
||||
.and("contain", "No primary key constraint found")
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import filterTests from "../../support/filterTests"
|
||||
|
||||
filterTests(["all"], () => {
|
||||
context("Oracle Datasource Testing", () => {
|
||||
xcontext("Oracle Datasource Testing", () => {
|
||||
if (Cypress.env("TEST_ENV")) {
|
||||
before(() => {
|
||||
cy.login()
|
||||
|
@ -11,8 +11,8 @@ filterTests(["all"], () => {
|
|||
const queryName = "Cypress Test Query"
|
||||
const queryRename = "CT Query Rename"
|
||||
|
||||
it("Should add Oracle data source and skip table fetch", () => {
|
||||
// Select Oracle data source
|
||||
it("Should add Oracle datasource and skip table fetch", () => {
|
||||
// Select Oracle datasource
|
||||
cy.selectExternalDatasource(datasource)
|
||||
// Skip table fetch - no config added
|
||||
cy.get(".spectrum-Button")
|
||||
|
@ -23,7 +23,7 @@ filterTests(["all"], () => {
|
|||
cy.get(".spectrum-Textfield-input", { timeout: 500 })
|
||||
.eq(1)
|
||||
.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.addDatasourceConfig(datasource, true)
|
||||
// Confirm config and no tables
|
||||
|
@ -33,8 +33,8 @@ filterTests(["all"], () => {
|
|||
cy.get(".spectrum-Body").eq(2).should("contain", "No tables found.")
|
||||
})
|
||||
|
||||
it("Should add Oracle data source and fetch tables without configuration", () => {
|
||||
// Select Oracle data source
|
||||
it("Should add Oracle datasource and fetch tables without configuration", () => {
|
||||
// Select Oracle datasource
|
||||
cy.selectExternalDatasource(datasource)
|
||||
// Attempt to fetch tables without applying configuration
|
||||
cy.intercept("**/datasources").as("datasource")
|
||||
|
@ -49,8 +49,8 @@ filterTests(["all"], () => {
|
|||
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true })
|
||||
})
|
||||
|
||||
xit("should add Oracle data source and fetch tables", () => {
|
||||
// Add & configure Oracle data source
|
||||
xit("should add Oracle datasource and fetch tables", () => {
|
||||
// Add & configure Oracle datasource
|
||||
cy.selectExternalDatasource(datasource)
|
||||
cy.intercept("**/datasources").as("datasource")
|
||||
cy.addDatasourceConfig(datasource)
|
||||
|
|
|
@ -11,8 +11,8 @@ filterTests(["all"], () => {
|
|||
const queryName = "Cypress Test Query"
|
||||
const queryRename = "CT Query Rename"
|
||||
|
||||
xit("Should add PostgreSQL data source without configuration", () => {
|
||||
// Select PostgreSQL data source
|
||||
xit("Should add PostgreSQL datasource without configuration", () => {
|
||||
// Select PostgreSQL datasource
|
||||
cy.selectExternalDatasource(datasource)
|
||||
// Attempt to fetch tables without applying configuration
|
||||
cy.intercept("**/datasources").as("datasource")
|
||||
|
@ -27,8 +27,8 @@ filterTests(["all"], () => {
|
|||
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true })
|
||||
})
|
||||
|
||||
it("should add PostgreSQL data source and fetch tables", () => {
|
||||
// Add & configure PostgreSQL data source
|
||||
it("should add PostgreSQL datasource and fetch tables", () => {
|
||||
// Add & configure PostgreSQL datasource
|
||||
cy.selectExternalDatasource(datasource)
|
||||
cy.intercept("**/datasources").as("datasource")
|
||||
cy.addDatasourceConfig(datasource)
|
||||
|
@ -162,7 +162,7 @@ filterTests(["all"], () => {
|
|||
switchSchema("randomText")
|
||||
|
||||
// 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
|
||||
cy.get(".spectrum-Table").should("contain", queryName)
|
||||
|
@ -173,7 +173,7 @@ filterTests(["all"], () => {
|
|||
switchSchema("1")
|
||||
|
||||
// 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")
|
||||
.eq(0)
|
||||
.find(".spectrum-Table-row")
|
||||
|
@ -187,7 +187,7 @@ filterTests(["all"], () => {
|
|||
switchSchema("public")
|
||||
|
||||
// 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")
|
||||
.eq(0)
|
||||
.find(".spectrum-Table-row")
|
||||
|
|
|
@ -10,11 +10,11 @@ filterTests(["smoke", "all"], () => {
|
|||
const datasource = "REST"
|
||||
const restUrl = "https://api.openbrewerydb.org/breweries"
|
||||
|
||||
it("Should add REST data source with incorrect API", () => {
|
||||
// Select REST data source
|
||||
it("Should add REST datasource with incorrect API", () => {
|
||||
// Select REST datasource
|
||||
cy.selectExternalDatasource(datasource)
|
||||
// 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.get("input").clear().type("random text")
|
||||
cy.get(".spectrum-Button").contains("Send").click({ force: true })
|
||||
|
|
|
@ -4,7 +4,7 @@ Cypress.on("uncaught:exception", () => {
|
|||
|
||||
// ACCOUNTS & USERS
|
||||
Cypress.Commands.add("login", (email, password) => {
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 10000 })
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 30000 })
|
||||
cy.url()
|
||||
.should("include", "/builder/")
|
||||
.then(url => {
|
||||
|
@ -33,7 +33,7 @@ Cypress.Commands.add("login", (email, password) => {
|
|||
})
|
||||
|
||||
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(".spectrum-Popover[data-cy='user-menu']").within(() => {
|
||||
cy.get("li[data-cy='user-logout']").click({ force: true })
|
||||
|
@ -43,7 +43,7 @@ Cypress.Commands.add("logOut", () => {
|
|||
|
||||
Cypress.Commands.add("logoutNoAppGrid", () => {
|
||||
// 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(".spectrum-Popover[data-cy='user-menu']").within(() => {
|
||||
cy.get(".spectrum-Menu-item").contains("Log out").click({ force: true })
|
||||
|
@ -68,11 +68,14 @@ Cypress.Commands.add("createUser", (email, permission) => {
|
|||
.click({ force: true })
|
||||
})
|
||||
}
|
||||
// Add user and wait for modal to change
|
||||
cy.get(".spectrum-Button").contains("Add user").click({ force: true })
|
||||
// Add user
|
||||
cy.get(".spectrum-Button").contains("Add users").click({ force: true })
|
||||
cy.get(".spectrum-ActionButton").contains("Add email").should("not.exist")
|
||||
})
|
||||
// Onboarding modal
|
||||
cy.get(".spectrum-Dialog-grid", { timeout: 5000 }).contains(
|
||||
"Choose your onboarding"
|
||||
)
|
||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||
cy.get(".onboarding-type").eq(1).click()
|
||||
cy.get(".spectrum-Button").contains("Done").click({ force: true })
|
||||
|
@ -163,7 +166,7 @@ Cypress.Commands.add("createApp", (name, addDefaultTable) => {
|
|||
const shouldCreateDefaultTable =
|
||||
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.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 => {
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 })
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 30000 })
|
||||
cy.wait(2000)
|
||||
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
||||
.its("body")
|
||||
|
@ -254,7 +257,7 @@ Cypress.Commands.add("deleteApp", name => {
|
|||
})
|
||||
|
||||
Cypress.Commands.add("deleteAllApps", () => {
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 })
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 30000 })
|
||||
cy.wait(500)
|
||||
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`, {
|
||||
timeout: 5000,
|
||||
|
@ -351,7 +354,7 @@ Cypress.Commands.add("alterAppVersion", (appId, version) => {
|
|||
})
|
||||
|
||||
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`)
|
||||
.its("body")
|
||||
|
@ -386,7 +389,7 @@ Cypress.Commands.add("importApp", (exportFilePath, name) => {
|
|||
|
||||
// Filters visible with 1 or more
|
||||
Cypress.Commands.add("searchForApplication", appName => {
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 30000 })
|
||||
cy.wait(2000)
|
||||
|
||||
// No app filter functionality if only 1 app exists
|
||||
|
@ -409,7 +412,7 @@ Cypress.Commands.add("searchForApplication", appName => {
|
|||
|
||||
// Assumes there are no others
|
||||
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(".title").contains(appName).should("exist")
|
||||
})
|
||||
|
@ -454,8 +457,8 @@ Cypress.Commands.add("createTable", (tableName, initialTable) => {
|
|||
cy.get(".spectrum-ButtonGroup").contains("Create").click()
|
||||
})
|
||||
// Ensure modal has closed and table is created
|
||||
cy.get(".spectrum-Modal").should("not.exist")
|
||||
cy.get(".spectrum-Tabs-content", { timeout: 1000 }).should(
|
||||
cy.get(".spectrum-Modal", { timeout: 2000 }).should("not.exist")
|
||||
cy.get(".spectrum-Tabs-content", { timeout: 2000 }).should(
|
||||
"contain",
|
||||
tableName
|
||||
)
|
||||
|
@ -634,30 +637,32 @@ Cypress.Commands.add(
|
|||
(datasourceNames, accessLevelLabel) => {
|
||||
cy.contains("Design").click()
|
||||
cy.get(".spectrum-Button").contains("Add screen").click({ force: true })
|
||||
cy.get(".spectrum-Modal").within(() => {
|
||||
cy.get(".item").contains("Autogenerated screens").click()
|
||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||
cy.get("[data-cy='autogenerated-screens']").click()
|
||||
cy.intercept("**/api/datasources").as("autoScreens")
|
||||
cy.get(".spectrum-Button").contains("Continue").click({ force: true })
|
||||
cy.wait("@autoScreens")
|
||||
cy.wait(5000)
|
||||
})
|
||||
cy.get(".spectrum-Modal [data-cy='data-source-modal']", {
|
||||
timeout: 500,
|
||||
}).within(() => {
|
||||
cy.get("[data-cy='autogenerated-screens']").should("not.exist")
|
||||
cy.get("[data-cy='data-source-modal']", { timeout: 10000 }).within(() => {
|
||||
for (let i = 0; i < datasourceNames.length; i++) {
|
||||
cy.wait(500)
|
||||
cy.get(".data-source-entry").contains(datasourceNames[i]).click()
|
||||
//Ensure the check mark is visible
|
||||
cy.get(".data-source-entry")
|
||||
.contains(datasourceNames[i], { timeout: 20000 })
|
||||
.click({ force: true })
|
||||
// Ensure the check mark is visible
|
||||
cy.get(".data-source-entry")
|
||||
.contains(datasourceNames[i])
|
||||
.get(".data-source-check")
|
||||
.get(".data-source-check", { timeout: 20000 })
|
||||
.should("exist")
|
||||
}
|
||||
|
||||
cy.get(".spectrum-Button").contains("Confirm").click({ force: true })
|
||||
})
|
||||
|
||||
cy.get(".spectrum-Modal").within(() => {
|
||||
cy.get(".spectrum-Modal", { timeout: 10000 }).within(() => {
|
||||
if (accessLevelLabel) {
|
||||
cy.get(".spectrum-Picker-label").click()
|
||||
cy.wait(500)
|
||||
cy.get(".spectrum-Picker-label", { timeout: 10000 }).click()
|
||||
cy.contains(accessLevelLabel).click()
|
||||
}
|
||||
cy.get(".spectrum-Button").contains("Done").click({ force: true })
|
||||
|
@ -758,7 +763,7 @@ Cypress.Commands.add("navigateToDataSection", () => {
|
|||
})
|
||||
|
||||
Cypress.Commands.add("navigateToAutogeneratedModal", () => {
|
||||
// Screen name must already exist within data source
|
||||
// Screen name must already exist within datasource
|
||||
cy.contains("Design").click()
|
||||
cy.get(".spectrum-Button").contains("Add screen").click({ force: true })
|
||||
cy.get(".spectrum-Modal").within(() => {
|
||||
|
@ -774,7 +779,7 @@ Cypress.Commands.add("navigateToAutogeneratedModal", () => {
|
|||
Cypress.Commands.add("selectExternalDatasource", datasourceName => {
|
||||
// Navigates to Data Section
|
||||
cy.navigateToDataSection()
|
||||
// Open Data Source modal
|
||||
// Open Datasource modal
|
||||
cy.get(".nav").within(() => {
|
||||
cy.get(".add-button").click()
|
||||
})
|
||||
|
@ -912,8 +917,9 @@ Cypress.Commands.add("createRestQuery", (method, restUrl, queryPrettyName) => {
|
|||
Cypress.Commands.add("closeModal", () => {
|
||||
cy.get(".spectrum-Modal", { timeout: 2000 }).within(() => {
|
||||
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", () => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/builder",
|
||||
"version": "1.3.21",
|
||||
"version": "1.3.22-alpha.4",
|
||||
"license": "GPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -71,10 +71,10 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.3.21",
|
||||
"@budibase/client": "^1.3.21",
|
||||
"@budibase/frontend-core": "^1.3.21",
|
||||
"@budibase/string-templates": "^1.3.21",
|
||||
"@budibase/bbui": "1.3.22-alpha.4",
|
||||
"@budibase/client": "1.3.22-alpha.4",
|
||||
"@budibase/frontend-core": "1.3.22-alpha.4",
|
||||
"@budibase/string-templates": "1.3.22-alpha.4",
|
||||
"@sentry/browser": "5.19.1",
|
||||
"@spectrum-css/page": "^3.0.1",
|
||||
"@spectrum-css/vars": "^3.0.1",
|
||||
|
@ -98,7 +98,7 @@
|
|||
"@babel/runtime": "^7.13.10",
|
||||
"@rollup/plugin-replace": "^2.4.2",
|
||||
"@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/svelte": "^3.0.0",
|
||||
"babel-jest": "^26.6.3",
|
||||
|
@ -120,7 +120,7 @@
|
|||
"ts-node": "^10.4.0",
|
||||
"tsconfig-paths": "4.0.0",
|
||||
"typescript": "^4.5.5",
|
||||
"vite": "^2.1.5"
|
||||
"vite": "^3.0.8"
|
||||
},
|
||||
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import { NotificationDisplay, BannerDisplay } from "@budibase/bbui"
|
||||
import { parse, stringify } from "qs"
|
||||
import HelpIcon from "components/common/HelpIcon.svelte"
|
||||
import LicensingOverlays from "components/portal/licensing/LicensingOverlays.svelte"
|
||||
|
||||
const queryHandler = { parse, stringify }
|
||||
</script>
|
||||
|
@ -12,6 +13,9 @@
|
|||
<BannerDisplay />
|
||||
|
||||
<NotificationDisplay />
|
||||
|
||||
<LicensingOverlays />
|
||||
|
||||
<Router {routes} config={{ queryHandler }} />
|
||||
<div class="modal-container" />
|
||||
<HelpIcon />
|
||||
|
|
|
@ -297,7 +297,10 @@ const getProviderContextBindings = (asset, dataProviders) => {
|
|||
schema = {}
|
||||
const values = context.values || []
|
||||
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") {
|
||||
// Schema contexts are generated dynamically depending on their data
|
||||
|
@ -357,6 +360,12 @@ const getProviderContextBindings = (asset, dataProviders) => {
|
|||
providerId,
|
||||
// Table ID is used by JSON fields to know what table the field is in
|
||||
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
|
||||
fieldSchema,
|
||||
providerId: "user",
|
||||
category: "Current User",
|
||||
icon: "User",
|
||||
display: fieldSchema,
|
||||
})
|
||||
})
|
||||
return bindings
|
||||
|
@ -399,11 +411,17 @@ const getDeviceBindings = () => {
|
|||
type: "context",
|
||||
runtimeBinding: `${safeDevice}.${makePropSafe("mobile")}`,
|
||||
readableBinding: `Device.Mobile`,
|
||||
category: "Device",
|
||||
icon: "DevicePhone",
|
||||
display: { type: "boolean", name: "mobile" },
|
||||
})
|
||||
bindings.push({
|
||||
type: "context",
|
||||
runtimeBinding: `${safeDevice}.${makePropSafe("tablet")}`,
|
||||
readableBinding: `Device.Tablet`,
|
||||
category: "Device",
|
||||
icon: "DevicePhone",
|
||||
display: { type: "boolean", name: "tablet" },
|
||||
})
|
||||
}
|
||||
return bindings
|
||||
|
@ -427,6 +445,8 @@ const getSelectedRowsBindings = asset => {
|
|||
"selectedRows"
|
||||
)}`,
|
||||
readableBinding: `${table._instanceName}.Selected rows`,
|
||||
category: "Selected rows",
|
||||
icon: "ViewRow",
|
||||
}))
|
||||
)
|
||||
|
||||
|
@ -458,6 +478,9 @@ const getStateBindings = () => {
|
|||
type: "context",
|
||||
runtimeBinding: `${safeState}.${makePropSafe(key)}`,
|
||||
readableBinding: `State.${key}`,
|
||||
category: "State",
|
||||
icon: "AutomatedSegment",
|
||||
display: { name: key },
|
||||
}))
|
||||
}
|
||||
return bindings
|
||||
|
@ -480,11 +503,17 @@ const getUrlBindings = asset => {
|
|||
type: "context",
|
||||
runtimeBinding: `${safeURL}.${makePropSafe(param)}`,
|
||||
readableBinding: `URL.${param}`,
|
||||
category: "URL",
|
||||
icon: "RailTop",
|
||||
display: { type: "string" },
|
||||
}))
|
||||
const queryParamsBinding = {
|
||||
type: "context",
|
||||
runtimeBinding: makePropSafe("query"),
|
||||
readableBinding: "Query params",
|
||||
category: "URL",
|
||||
icon: "RailTop",
|
||||
display: { type: "object" },
|
||||
}
|
||||
return urlParamBindings.concat([queryParamsBinding])
|
||||
}
|
||||
|
@ -495,6 +524,9 @@ const getRoleBindings = () => {
|
|||
type: "context",
|
||||
runtimeBinding: `trim "${role._id}"`,
|
||||
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
|
||||
// setting
|
||||
const component = findComponent(asset.props, componentId)
|
||||
const def = store.actions.components.getDefinition(component?._component)
|
||||
const settings = getComponentSettings(component?._component)
|
||||
const eventSetting = settings.find(setting => setting.key === settingKey)
|
||||
if (eventSetting?.context?.length) {
|
||||
|
@ -525,6 +558,8 @@ export const getEventContextBindings = (
|
|||
runtimeBinding: `${makePropSafe("eventContext")}.${makePropSafe(
|
||||
contextEntry.key
|
||||
)}`,
|
||||
category: component._instanceName,
|
||||
icon: def.icon,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -546,6 +581,8 @@ export const getEventContextBindings = (
|
|||
bindings.push({
|
||||
readableBinding: `Action ${idx + 1}.${contextValue.label}`,
|
||||
runtimeBinding: `actions.${idx}.${contextValue.value}`,
|
||||
category: "Actions",
|
||||
icon: "JourneyAction",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ function prepareData(config) {
|
|||
ds => ds.source === config.type
|
||||
).length
|
||||
|
||||
let baseName = IntegrationNames[config.type]
|
||||
let baseName = IntegrationNames[config.type] || config.name
|
||||
let name =
|
||||
existingTypeCount === 0 ? baseName : `${baseName}-${existingTypeCount + 1}`
|
||||
|
||||
|
@ -27,7 +27,7 @@ export async function saveDatasource(config, skipFetch = false) {
|
|||
// Create datasource
|
||||
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 datasources.select(resp._id)
|
||||
return resp
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { getFrontendStore } from "./store/frontend"
|
||||
import { getAutomationStore } from "./store/automation"
|
||||
import { getTemporalStore } from "./store/temporal"
|
||||
import { getThemeStore } from "./store/theme"
|
||||
import { derived } from "svelte/store"
|
||||
import { findComponent, findComponentPath } from "./componentUtils"
|
||||
|
@ -8,6 +9,7 @@ import { RoleUtils } from "@budibase/frontend-core"
|
|||
export const store = getFrontendStore()
|
||||
export const automationStore = getAutomationStore()
|
||||
export const themeStore = getThemeStore()
|
||||
export const temporalStore = getTemporalStore()
|
||||
|
||||
export const selectedScreen = derived(store, $store => {
|
||||
return $store.screens.find(screen => screen._id === $store.selectedScreenId)
|
||||
|
|
|
@ -90,13 +90,21 @@ export const getFrontendStore = () => {
|
|||
|
||||
// Fetch component definitions.
|
||||
// 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
|
||||
store.update(state => ({
|
||||
...state,
|
||||
libraries: application.componentLibraries,
|
||||
components,
|
||||
customComponents,
|
||||
clientFeatures: {
|
||||
...INITIAL_FRONTEND_STATE.clientFeatures,
|
||||
...components.features,
|
||||
|
@ -116,6 +124,7 @@ export const getFrontendStore = () => {
|
|||
version: application.version,
|
||||
revertableVersion: application.revertableVersion,
|
||||
navigation: application.navigation || {},
|
||||
usedPlugins: application.usedPlugins || [],
|
||||
}))
|
||||
|
||||
// Initialise backend stores
|
||||
|
@ -189,9 +198,18 @@ export const getFrontendStore = () => {
|
|||
})
|
||||
},
|
||||
save: async screen => {
|
||||
const state = get(store)
|
||||
const creatingNewScreen = screen._id === undefined
|
||||
const savedScreen = await API.saveScreen(screen)
|
||||
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 => {
|
||||
// Update screen object
|
||||
const idx = state.screens.findIndex(x => x._id === savedScreen._id)
|
||||
|
@ -210,6 +228,9 @@ export const getFrontendStore = () => {
|
|||
// Update routes
|
||||
state.routes = routesResponse.routes
|
||||
|
||||
// Update used plugins
|
||||
state.usedPlugins = usedPlugins
|
||||
|
||||
return state
|
||||
})
|
||||
return savedScreen
|
||||
|
@ -368,9 +389,6 @@ export const getFrontendStore = () => {
|
|||
if (!componentName) {
|
||||
return null
|
||||
}
|
||||
if (!componentName.startsWith("@budibase")) {
|
||||
componentName = `@budibase/standard-components/${componentName}`
|
||||
}
|
||||
return get(store).components[componentName]
|
||||
},
|
||||
createInstance: (componentName, presetProps) => {
|
||||
|
|
|
@ -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 },
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@
|
|||
customQueryIconColor,
|
||||
customQueryText,
|
||||
} from "helpers/data/utils"
|
||||
import ICONS from "./icons"
|
||||
import { getIcon } from "./icons"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
|
||||
let openDataSources = []
|
||||
|
@ -32,8 +32,8 @@
|
|||
: []
|
||||
$: openDataSource = enrichedDataSources.find(x => x.open)
|
||||
$: {
|
||||
// Ensure the open data source is always included in the list of open
|
||||
// data sources
|
||||
// Ensure the open datasource is always included in the list of open
|
||||
// datasources
|
||||
if (openDataSource) {
|
||||
openNode(openDataSource)
|
||||
}
|
||||
|
@ -79,7 +79,7 @@
|
|||
})
|
||||
|
||||
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) {
|
||||
return true
|
||||
}
|
||||
|
@ -124,7 +124,7 @@
|
|||
>
|
||||
<div class="datasource-icon" slot="icon">
|
||||
<svelte:component
|
||||
this={ICONS[datasource.source]}
|
||||
this={getIcon(datasource.source, datasource.schema)}
|
||||
height="18"
|
||||
width="18"
|
||||
/>
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
notifications,
|
||||
Modal,
|
||||
Table,
|
||||
Toggle,
|
||||
} from "@budibase/bbui"
|
||||
import { datasources, integrations, tables } from "stores/backend"
|
||||
import CreateEditRelationship from "components/backend/Datasources/CreateEditRelationship.svelte"
|
||||
|
@ -15,6 +16,7 @@
|
|||
import ArrayRenderer from "components/common/renderers/ArrayRenderer.svelte"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import { goto } from "@roxi/routify"
|
||||
import ValuesList from "components/common/ValuesList.svelte"
|
||||
|
||||
export let datasource
|
||||
export let save
|
||||
|
@ -31,6 +33,8 @@
|
|||
let createExternalTableModal
|
||||
let selectedFromRelationship, selectedToRelationship
|
||||
let confirmDialog
|
||||
let specificTables = null
|
||||
let requireSpecificTables = false
|
||||
|
||||
$: integration = datasource && $integrations[datasource.source]
|
||||
$: plusTables = datasource?.plus
|
||||
|
@ -87,7 +91,7 @@
|
|||
|
||||
async function updateDatasourceSchema() {
|
||||
try {
|
||||
await datasources.updateSchema(datasource)
|
||||
await datasources.updateSchema(datasource, specificTables)
|
||||
notifications.success(`Datasource ${name} tables updated successfully.`)
|
||||
await tables.fetch()
|
||||
} catch (error) {
|
||||
|
@ -150,6 +154,19 @@
|
|||
warning={false}
|
||||
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>
|
||||
If you have fetched tables from this database before, this action may
|
||||
overwrite any changes you made after your initial fetch.
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -15,8 +15,9 @@ import GoogleSheets from "./GoogleSheets.svelte"
|
|||
import Firebase from "./Firebase.svelte"
|
||||
import Redis from "./Redis.svelte"
|
||||
import Snowflake from "./Snowflake.svelte"
|
||||
import Custom from "./Custom.svelte"
|
||||
|
||||
export default {
|
||||
const ICONS = {
|
||||
BUDIBASE: Budibase,
|
||||
POSTGRES: Postgres,
|
||||
DYNAMODB: DynamoDB,
|
||||
|
@ -34,4 +35,15 @@ export default {
|
|||
FIRESTORE: Firebase,
|
||||
REDIS: Redis,
|
||||
SNOWFLAKE: Snowflake,
|
||||
CUSTOM: Custom,
|
||||
}
|
||||
|
||||
export default ICONS
|
||||
|
||||
export function getIcon(integrationType, schema) {
|
||||
if (schema?.custom || !ICONS[integrationType]) {
|
||||
return ICONS.CUSTOM
|
||||
} else {
|
||||
return ICONS[integrationType]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
import { createRestDatasource } from "builderStore/datasource"
|
||||
import { goto } from "@roxi/routify"
|
||||
import ImportRestQueriesModal from "./ImportRestQueriesModal.svelte"
|
||||
import DatasourceCard from "../_components/DatasourceCard.svelte"
|
||||
|
||||
export let modal
|
||||
let integrations = {}
|
||||
|
@ -27,6 +28,9 @@
|
|||
let importModal
|
||||
|
||||
$: showImportButton = false
|
||||
$: customIntegrations = Object.entries(integrations).filter(
|
||||
entry => entry[1].custom
|
||||
)
|
||||
|
||||
checkShowImport()
|
||||
|
||||
|
@ -49,6 +53,9 @@
|
|||
schema: selected.datasource,
|
||||
auth: selected.auth,
|
||||
}
|
||||
if (selected.friendlyName) {
|
||||
integration.name = selected.friendlyName
|
||||
}
|
||||
checkShowImport()
|
||||
}
|
||||
|
||||
|
@ -119,7 +126,7 @@
|
|||
<Modal bind:this={modal}>
|
||||
<ModalContent
|
||||
disabled={!Object.keys(integration).length}
|
||||
title="Add data source"
|
||||
title="Add datasource"
|
||||
confirmText="Continue"
|
||||
showSecondaryButton={showImportButton}
|
||||
secondaryButtonText="Import"
|
||||
|
@ -148,38 +155,41 @@
|
|||
</Layout>
|
||||
|
||||
<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">
|
||||
{#each Object.entries(integrations).filter(([key]) => key !== IntegrationTypes.INTERNAL) as [integrationType, schema]}
|
||||
<div
|
||||
class:selected={integration.type === integrationType}
|
||||
on:click={() => selectIntegration(integrationType)}
|
||||
class="item hoverable"
|
||||
>
|
||||
<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 Object.entries(integrations).filter(([key, val]) => key !== IntegrationTypes.INTERNAL && !val.custom) as [integrationType, schema]}
|
||||
<DatasourceCard
|
||||
on:selected={evt => selectIntegration(evt.detail)}
|
||||
{schema}
|
||||
bind:integrationType
|
||||
{integration}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</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>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.item-list {
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -14,8 +14,14 @@
|
|||
let datasource = cloneDeep(integration)
|
||||
let skipFetch = false
|
||||
|
||||
$: name =
|
||||
IntegrationNames[datasource.type] || datasource.name || datasource.type
|
||||
|
||||
async function saveDatasource() {
|
||||
try {
|
||||
if (!datasource.name) {
|
||||
datasource.name = name
|
||||
}
|
||||
const resp = await save(datasource, skipFetch)
|
||||
$goto(`./datasource/${resp._id}`)
|
||||
notifications.success(`Datasource updated successfully.`)
|
||||
|
@ -32,7 +38,7 @@
|
|||
</script>
|
||||
|
||||
<ModalContent
|
||||
title={`Connect to ${IntegrationNames[datasource.type]}`}
|
||||
title={`Connect to ${name}`}
|
||||
onConfirm={() => saveDatasource()}
|
||||
onCancel={() => modal.show()}
|
||||
confirmText={datasource.plus
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
} from "@budibase/bbui"
|
||||
import TemplateCard from "components/common/TemplateCard.svelte"
|
||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||
import { licensing } from "stores/portal"
|
||||
|
||||
export let templates
|
||||
|
||||
|
@ -96,15 +97,17 @@
|
|||
backgroundColour={templateEntry.background}
|
||||
icon={templateEntry.icon}
|
||||
>
|
||||
<Button
|
||||
cta
|
||||
on:click={() => {
|
||||
template = templateEntry
|
||||
creationModal.show()
|
||||
}}
|
||||
>
|
||||
Use template
|
||||
</Button>
|
||||
{#if !($licensing?.usageMetrics?.apps >= 100)}
|
||||
<Button
|
||||
cta
|
||||
on:click={() => {
|
||||
template = templateEntry
|
||||
creationModal.show()
|
||||
}}
|
||||
>
|
||||
Use template
|
||||
</Button>
|
||||
{/if}
|
||||
<a
|
||||
href={templateEntry.url}
|
||||
target="_blank"
|
||||
|
|
|
@ -9,6 +9,9 @@
|
|||
Body,
|
||||
Layout,
|
||||
Button,
|
||||
ActionButton,
|
||||
Icon,
|
||||
Popover,
|
||||
} from "@budibase/bbui"
|
||||
import { createEventDispatcher, onMount } from "svelte"
|
||||
import {
|
||||
|
@ -45,9 +48,25 @@
|
|||
let jsValue = initialValueJS ? value : null
|
||||
let hbsValue = initialValueJS ? null : value
|
||||
|
||||
let selectedCategory = null
|
||||
|
||||
let popover
|
||||
let popoverAnchor
|
||||
let hoverTarget
|
||||
|
||||
$: usingJS = mode === "JavaScript"
|
||||
$: searchRgx = new RegExp(search, "ig")
|
||||
$: 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
|
||||
.map(([name, categoryBindings]) => ({
|
||||
name,
|
||||
|
@ -55,10 +74,19 @@
|
|||
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 => {
|
||||
return helper.label.match(searchRgx) || helper.description.match(searchRgx)
|
||||
})
|
||||
|
||||
$: categoryNames = [...categories.map(cat => cat[0]), "Helpers"]
|
||||
|
||||
$: codeMirrorHints = bindings?.map(x => `$("${x.readableBinding}")`)
|
||||
|
||||
const updateValue = val => {
|
||||
|
@ -140,58 +168,163 @@
|
|||
})
|
||||
</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>
|
||||
<svelte:fragment slot="sidebar">
|
||||
<div class="container">
|
||||
<section>
|
||||
<Layout noPadding gap="S">
|
||||
{#if selectedCategory}
|
||||
<div>
|
||||
<ActionButton
|
||||
secondary
|
||||
icon={"ArrowLeft"}
|
||||
on:click={() => {
|
||||
selectedCategory = null
|
||||
}}
|
||||
>
|
||||
Back
|
||||
</ActionButton>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !selectedCategory}
|
||||
<div class="heading">Search</div>
|
||||
<Search placeholder="Search" bind:value={search} />
|
||||
</section>
|
||||
{#each filteredCategories as category}
|
||||
{#if category.bindings?.length}
|
||||
<section>
|
||||
<div class="heading">{category.name}</div>
|
||||
{/if}
|
||||
|
||||
{#if !selectedCategory && !search}
|
||||
<ul class="category-list">
|
||||
{#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>
|
||||
{#each category.bindings as binding}
|
||||
<li on:click={() => addBinding(binding)}>
|
||||
<span class="binding__label">{binding.readableBinding}</span>
|
||||
{#if binding.type}
|
||||
<span class="binding__type">{binding.type}</span>
|
||||
{/if}
|
||||
{#if binding.description}
|
||||
<br />
|
||||
<div class="binding__description">
|
||||
{binding.description || ""}
|
||||
</div>
|
||||
<li
|
||||
class="binding"
|
||||
on:mouseenter={e => {
|
||||
popoverAnchor = e.target
|
||||
if (!binding.description) {
|
||||
return
|
||||
}
|
||||
hoverTarget = {
|
||||
title: binding.display.name || binding.fieldSchema.name,
|
||||
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}
|
||||
</li>
|
||||
{/each}
|
||||
</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}
|
||||
{/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}
|
||||
</div>
|
||||
</Layout>
|
||||
</svelte:fragment>
|
||||
<div class="main">
|
||||
<Tabs selected={mode} on:select={onChangeMode}>
|
||||
|
@ -241,6 +374,35 @@
|
|||
</DrawerContent>
|
||||
|
||||
<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) {
|
||||
min-height: 202px !important;
|
||||
}
|
||||
|
@ -251,23 +413,20 @@
|
|||
padding: var(--spacing-s) var(--spacing-xl);
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: calc(-1 * var(--spacing-xl));
|
||||
}
|
||||
.heading {
|
||||
.heading,
|
||||
.cat-heading {
|
||||
font-size: var(--font-size-s);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
padding: var(--spacing-xl) 0 var(--spacing-m) 0;
|
||||
}
|
||||
|
||||
section {
|
||||
padding: 0 var(--spacing-xl) var(--spacing-xl) var(--spacing-xl);
|
||||
}
|
||||
section:not(:first-child) {
|
||||
border-top: var(--border-light);
|
||||
.cat-heading {
|
||||
display: flex;
|
||||
gap: var(--spacing-m);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
|
@ -278,7 +437,7 @@
|
|||
font-size: var(--font-size-s);
|
||||
padding: var(--spacing-m);
|
||||
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,
|
||||
border-color 130ms ease-in-out;
|
||||
word-wrap: break-word;
|
||||
|
@ -292,22 +451,14 @@
|
|||
li:hover {
|
||||
color: var(--spectrum-global-color-gray-900);
|
||||
background-color: var(--spectrum-global-color-gray-50);
|
||||
border-color: var(--spectrum-global-color-gray-500);
|
||||
cursor: pointer;
|
||||
}
|
||||
li:hover :global(*) {
|
||||
color: var(--spectrum-global-color-gray-900) !important;
|
||||
}
|
||||
|
||||
.binding__label {
|
||||
font-weight: 600;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.binding__description {
|
||||
color: var(--spectrum-global-color-gray-700);
|
||||
margin: 0.5rem 0 0 0;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.binding__type {
|
||||
font-family: monospace;
|
||||
background-color: var(--spectrum-global-color-gray-200);
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
}
|
||||
return bindings?.map(binding => ({
|
||||
...binding,
|
||||
category: "Bindable Values",
|
||||
type: null,
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -73,13 +73,13 @@
|
|||
|
||||
<div class="root">
|
||||
<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 />
|
||||
You can always add or override fields manually.
|
||||
</Body>
|
||||
|
||||
<div class="params">
|
||||
<Label small>Data Source</Label>
|
||||
<Label small>Datasource</Label>
|
||||
<Select
|
||||
bind:value={parameters.providerId}
|
||||
options={providerOptions}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue