Merge pull request #7826 from Budibase/develop

Develop -> Master
This commit is contained in:
Martin McKeaveney 2022-09-20 13:16:44 +01:00 committed by GitHub
commit 93df630f2c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
338 changed files with 14369 additions and 5373 deletions

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

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

View File

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

3
.gitignore vendored
View File

@ -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
@ -103,3 +104,5 @@ stats.html
# TypeScript cache
*.tsbuildinfo
budibase-component
budibase-datasource

View File

@ -130,6 +130,10 @@ spec:
- name: BB_ADMIN_USER_PASSWORD
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
value: {{ .Values.services.apps.nodeDebug | quote }}
@ -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 }}

View File

@ -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 }}

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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 }}

View File

@ -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:

View File

@ -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"

View File

@ -2020,10 +2020,10 @@ node-domexception@^1.0.0:
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
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"

View File

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

View File

@ -25,9 +25,12 @@ services:
REDIS_PASSWORD: ${REDIS_PASSWORD}
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

View File

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

View File

@ -80,6 +80,20 @@ http {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
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;

View File

@ -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;

View File

@ -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;

View File

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

View File

@ -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",

View File

@ -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",

View File

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

View File

@ -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",
}

View File

@ -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"

View File

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

View File

@ -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) => {

View File

@ -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

View File

@ -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) {

View File

@ -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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
const http = require("./http")
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

View File

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

View File

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

View File

@ -5,8 +5,15 @@ import {
DatasourceCreatedEvent,
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)
}

View File

@ -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"

View File

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

View File

@ -31,20 +31,26 @@ const TENANT_FEATURE_FLAGS = getFeatureFlags()
exports.isEnabled = featureFlag => {
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 = {

View File

@ -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

View File

@ -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,

View File

@ -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)

View File

@ -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,

View File

@ -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 {

View File

@ -57,7 +57,11 @@ function publicPolicy(bucketName: any) {
}
}
const PUBLIC_BUCKETS = [ObjectStoreBuckets.APPS, ObjectStoreBuckets.GLOBAL]
const PUBLIC_BUCKETS = [
ObjectStoreBuckets.APPS,
ObjectStoreBuckets.GLOBAL,
ObjectStoreBuckets.PLUGINS,
]
/**
* Gets a connection to the object store using the S3 SDK.
@ -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}`)
}

View File

@ -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 () {

View File

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

View File

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

View File

@ -2,28 +2,12 @@ const redis = require("../redis/init")
const { v4: uuidv4 } = require("uuid")
const { 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

View File

@ -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

View File

@ -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

View File

@ -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}`)
}
exports.searchGlobalUsersByApp = async (appId, opts) => {
return response
}
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}`,

View File

@ -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.

View File

@ -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"

View File

@ -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",

View File

@ -1,4 +1,4 @@
export default function positionDropdown(element, { anchor, align }) {
export default function positionDropdown(element, { anchor, align, maxWidth }) {
let positionSide = "top"
let 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()

View File

@ -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>

View File

@ -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;

View File

@ -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>

View File

@ -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

View File

@ -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 || "")}

View File

@ -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>

View File

@ -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>

View File

@ -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,
}
}

View File

@ -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>

View File

@ -34,6 +34,7 @@ export { default as Layout } from "./Layout/Layout.svelte"
export { default as Page } from "./Layout/Page.svelte"
export { default as 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"

View File

@ -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(() => {

View File

@ -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)

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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")

View File

@ -14,7 +14,7 @@ filterTests(["smoke", "all"], () => {
// 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 })

View File

@ -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()
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 })
@ -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", () => {

View File

@ -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"
}

View File

@ -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 />

View File

@ -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",
})
})
}

View File

@ -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}`

View File

@ -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)

View File

@ -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) => {

View File

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

View File

@ -13,7 +13,7 @@
customQueryIconColor,
customQueryText,
} from "helpers/data/utils"
import ICONS from "./icons"
import { getIcon } from "./icons"
import { notifications } from "@budibase/bbui"
let openDataSources = []
@ -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"
/>

View File

@ -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.

View File

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

View File

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

View File

@ -15,8 +15,9 @@ import GoogleSheets from "./GoogleSheets.svelte"
import Firebase from "./Firebase.svelte"
import 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]
}
}

View File

@ -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()
}
@ -150,36 +157,39 @@
<Layout noPadding gap="XS">
<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"
{#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}
/>
<div class="text">
<Heading size="XXS">{schema.friendlyName}</Heading>
{#if schema.type}
<Detail size="S">{schema.type || ""}</Detail>
{/if}
</div>
</div>
</div>
{/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);
}

View File

@ -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

View File

@ -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,6 +97,7 @@
backgroundColour={templateEntry.background}
icon={templateEntry.icon}
>
{#if !($licensing?.usageMetrics?.apps >= 100)}
<Button
cta
on:click={() => {
@ -105,6 +107,7 @@
>
Use template
</Button>
{/if}
<a
href={templateEntry.url}
target="_blank"

View File

@ -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>
{/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}
<section>
<div class="heading">{category.name}</div>
<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>
<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}
{#if binding.description}
<br />
<div class="binding__description">
{binding.description || ""}
</div>
</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}
<section>
<div class="heading">Helpers</div>
<ul>
<ul class="helpers">
{#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
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>
</section>
{/if}
</div>
{/if}
{/if}
</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);

View File

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

View File

@ -79,7 +79,7 @@
</Body>
<div class="params">
<Label small>Data Source</Label>
<Label small>Datasource</Label>
<Select
bind:value={parameters.providerId}
options={providerOptions}

View File

@ -71,13 +71,13 @@
<div class="root">
<Body size="S">
Choosing a Data Source will automatically use the data it provides, but it's
Choosing a Datasource will automatically use the data it provides, but it's
optional.<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}

View File

@ -0,0 +1,49 @@
<script>
import { Modal, ModalContent, Body } from "@budibase/bbui"
import { auth, admin } from "stores/portal"
export let onDismiss = () => {}
export let onShow = () => {}
let accountDowngradeModal
$: accountUrl = $admin.accountPortalUrl
$: upgradeUrl = `${accountUrl}/portal/upgrade`
export function show() {
accountDowngradeModal.show()
}
export function hide() {
accountDowngradeModal.hide()
}
</script>
<Modal bind:this={accountDowngradeModal} on:show={onShow} on:hide={onDismiss}>
<ModalContent
title="Your account is now on the Free plan"
size="M"
showCancelButton={$auth.user.accountPortalAccess}
confirmText={$auth.user.accountPortalAccess ? "Upgrade" : "Confirm"}
onConfirm={$auth.user.accountPortalAccess
? () => {
window.location.href = upgradeUrl
}
: null}
>
<Body>
The payment for your subscription has failed and we have downgraded your
account to the <span class="free-plan">Free plan</span>.
</Body>
<Body>Upgrade to restore full functionality.</Body>
{#if !$auth.user.accountPortalAccess}
<Body>Please contact the account holder to upgrade.</Body>
{/if}
</ModalContent>
</Modal>
<style>
.free-plan {
font-weight: 600;
}
</style>

View File

@ -0,0 +1,47 @@
<script>
import { Modal, ModalContent, Body } from "@budibase/bbui"
import { auth, admin } from "stores/portal"
export let onDismiss = () => {}
let appLimitModal
$: accountUrl = $admin.accountPortalUrl
$: upgradeUrl = `${accountUrl}/portal/upgrade`
export function show() {
appLimitModal.show()
}
export function hide() {
appLimitModal.hide()
}
</script>
<Modal bind:this={appLimitModal} on:hide={onDismiss}>
<ModalContent
title="Upgrade to get more apps "
size="M"
showCancelButton={false}
confirmText={$auth.user.accountPortalAccess ? "Upgrade" : "Confirm"}
onConfirm={$auth.user.accountPortalAccess
? () => {
window.location.href = upgradeUrl
}
: null}
>
<Body>
You are currently on our <span class="free-plan">Free plan</span>. Upgrade
to our Pro plan to get unlimited apps and additional features.
</Body>
{#if !$auth.user.accountPortalAccess}
<Body>Please contact the account holder to upgrade.</Body>
{/if}
</ModalContent>
</Modal>
<style>
.free-plan {
font-weight: 600;
}
</style>

View File

@ -0,0 +1,78 @@
<script>
import { Modal, ModalContent, Body, TooltipWrapper } from "@budibase/bbui"
import { licensing, auth, admin } from "stores/portal"
export let onDismiss = () => {}
export let onShow = () => {}
let dayPassModal
$: accountUrl = $admin.accountPortalUrl
$: upgradeUrl = `${accountUrl}/portal/upgrade`
$: daysRemaining = $licensing.quotaResetDaysRemaining
$: quotaResetDate = $licensing.quotaResetDate
$: dayPassesUsed = $licensing.usageMetrics?.dayPasses
$: dayPassesTitle =
dayPassesUsed >= 100
? "You have run out of Day Passes"
: "You are almost out of Day Passes"
$: dayPassesBody =
dayPassesUsed >= 100
? "Upgrade your account to bring your apps back online."
: "Upgrade your account to prevent your apps from going offline."
export function show() {
dayPassModal.show()
}
export function hide() {
dayPassModal.hide()
}
</script>
<Modal bind:this={dayPassModal} on:show={onShow} on:hide={onDismiss}>
{#if $auth.user.accountPortalAccess}
<ModalContent
title={dayPassesTitle}
size="M"
confirmText="Upgrade"
onConfirm={() => {
window.location.href = upgradeUrl
}}
>
<Body>
You have used <span class="daypass_percent">{dayPassesUsed}%</span> of
your plans Day Passes with {daysRemaining} day{daysRemaining == 1
? ""
: "s"} remaining.
<span class="tooltip">
<TooltipWrapper tooltip={quotaResetDate} size="S" />
</span>
</Body>
<Body>{dayPassesBody}</Body>
</ModalContent>
{:else}
<ModalContent title={dayPassesTitle} size="M" showCancelButton={false}>
<Body>
You have used <span class="daypass_percent">{dayPassesUsed}%</span> of
your plans Day Passes with {daysRemaining} day{daysRemaining == 1
? ""
: "s"} remaining.
<span class="tooltip">
<TooltipWrapper tooltip={quotaResetDate} size="S" />
</span>
</Body>
<Body>Please contact your account holder to upgrade.</Body>
</ModalContent>
{/if}
</Modal>
<style>
.tooltip {
display: inline-block;
}
.tooltip :global(.icon-container) {
margin: 0px;
}
</style>

View File

@ -0,0 +1,118 @@
<script>
import { licensing, auth } from "stores/portal"
import { temporalStore } from "builderStore"
import { onMount } from "svelte"
import DayPassWarningModal from "./DayPassWarningModal.svelte"
import PaymentFailedModal from "./PaymentFailedModal.svelte"
import AccountDowngradedModal from "./AccountDowngradedModal.svelte"
import { ExpiringKeys } from "./constants"
import { getBanners } from "./licensingBanners"
import { banner } from "@budibase/bbui"
import { FEATURE_FLAGS, isEnabled } from "../../../helpers/featureFlags"
const oneDayInSeconds = 86400
let queuedBanners = []
let queuedModals = []
let dayPassModal
let paymentFailedModal
let accountDowngradeModal
let userLoaded = false
let loaded = false
let licensingLoaded = false
let currentModalCfg = null
const processModals = () => {
const defaultCacheFn = key => {
temporalStore.actions.setExpiring(key, {}, oneDayInSeconds)
}
const dismissableModals = [
{
key: ExpiringKeys.LICENSING_DAYPASS_WARNING_MODAL,
criteria: () => {
return $licensing?.usageMetrics?.dayPasses >= 90
},
action: () => {
dayPassModal.show()
},
cache: () => {
defaultCacheFn(ExpiringKeys.LICENSING_DAYPASS_WARNING_MODAL)
},
},
{
key: ExpiringKeys.LICENSING_PAYMENT_FAILED,
criteria: () => {
return $licensing.accountPastDue && !$licensing.isFreePlan()
},
action: () => {
paymentFailedModal.show()
},
cache: () => {
defaultCacheFn(ExpiringKeys.LICENSING_PAYMENT_FAILED)
},
},
{
key: ExpiringKeys.LICENSING_ACCOUNT_DOWNGRADED_MODAL,
criteria: () => {
return $licensing?.accountDowngraded
},
action: () => {
accountDowngradeModal.show()
},
cache: () => {
defaultCacheFn(ExpiringKeys.LICENSING_ACCOUNT_DOWNGRADED_MODAL)
},
},
]
return dismissableModals.filter(modal => {
return !temporalStore.actions.getExpiring(modal.key) && modal.criteria()
})
}
const showNextModal = () => {
if (currentModalCfg) {
currentModalCfg.cache()
}
if (queuedModals.length) {
currentModalCfg = queuedModals.shift()
currentModalCfg.action()
} else {
currentModalCfg = null
}
}
$: if (
userLoaded &&
licensingLoaded &&
loaded &&
isEnabled(FEATURE_FLAGS.LICENSING)
) {
queuedModals = processModals()
queuedBanners = getBanners()
showNextModal()
banner.queue(queuedBanners)
}
onMount(async () => {
auth.subscribe(state => {
if (state.user && !userLoaded) {
userLoaded = true
}
})
licensing.subscribe(state => {
if (state.usageMetrics && !licensingLoaded) {
licensingLoaded = true
}
})
loaded = true
})
</script>
<DayPassWarningModal bind:this={dayPassModal} onDismiss={showNextModal} />
<PaymentFailedModal bind:this={paymentFailedModal} onDismiss={showNextModal} />
<AccountDowngradedModal
bind:this={accountDowngradeModal}
onDismiss={showNextModal}
/>

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