Merge remote-tracking branch 'origin/develop' into fix/pc-fixes

This commit is contained in:
Peter Clement 2022-09-12 11:18:41 +01:00
commit 3cedc62bc2
273 changed files with 11379 additions and 7525 deletions

View File

@ -68,18 +68,28 @@ jobs:
]
env:
KUBECONFIG_FILE: '${{ secrets.RELEASE_KUBECONFIG }}'
- name: Set the base64 kubeconfig
run: |
base64_kubeconfig=$(echo ${{ secrets.RELEASE_KUBECONFIG }} | base64)
echo "RELEASE_KUBECONFIG=$base64_kubeconfig" >> $GITHUB_ENV
- name: Re roll the services
- name: Re roll app-service
uses: actions-hub/kubectl@master
env:
KUBE_CONFIG: ${{ env.RELEASE_KUBECONFIG }}
KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }}
with:
args: rollout restart deployment proxy-service -n budibase && kubectl rollout restart deployment app-service -n budibase && kubectl rollout restart deployment worker-service -n budibase
args: rollout restart deployment app-service -n budibase
- name: Re roll proxy-service
uses: actions-hub/kubectl@master
env:
KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }}
with:
args: rollout restart deployment proxy-service -n budibase
- name: Re roll worker-service
uses: actions-hub/kubectl@master
env:
KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }}
with:
args: rollout restart deployment worker-service -n budibase
- name: Discord Webhook Action
uses: tsickert/discord-webhook@v4.0.0

View File

@ -121,17 +121,26 @@ jobs:
env:
KUBECONFIG_FILE: '${{ secrets.RELEASE_KUBECONFIG }}'
- name: Set the base64 kubeconfig
run: |
base64_kubeconfig=$(echo ${{ secrets.RELEASE_KUBECONFIG }} | base64)
echo "RELEASE_KUBECONFIG=$base64_kubeconfig" >> $GITHUB_ENV
- name: Re roll the services
- name: Re roll app-service
uses: actions-hub/kubectl@master
env:
KUBE_CONFIG: ${{ env.RELEASE_KUBECONFIG }}
KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }}
with:
args: rollout restart deployment proxy-service -n budibase && kubectl rollout restart deployment app-service -n budibase && kubectl rollout restart deployment worker-service -n budibase
args: rollout restart deployment app-service -n budibase
- name: Re roll proxy-service
uses: actions-hub/kubectl@master
env:
KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }}
with:
args: rollout restart deployment proxy-service -n budibase
- name: Re roll worker-service
uses: actions-hub/kubectl@master
env:
KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }}
with:
args: rollout restart deployment worker-service -n budibase
- name: Discord Webhook Action
uses: tsickert/discord-webhook@v4.0.0

4
.gitignore vendored
View File

@ -102,4 +102,6 @@ packages/builder/cypress/reports
stats.html
# TypeScript cache
*.tsbuildinfo
*.tsbuildinfo
budibase-component
budibase-datasource

View File

@ -4,7 +4,7 @@
"singleQuote": false,
"trailingComma": "es5",
"arrowParens": "avoid",
"jsxBracketSameLine": false,
"bracketSameLine": false,
"plugins": ["prettier-plugin-svelte"],
"svelteSortOrder": "options-scripts-markup-styles"
}

View File

@ -124,11 +124,31 @@ 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
value: {{ .Values.services.apps.nodeDebug | quote }}
{{ end }}
{{ if .Values.globals.elasticApmEnabled }}
- name: ELASTIC_APM_ENABLED
value: {{ .Values.globals.elasticApmEnabled | quote }}
{{ end }}
{{ if .Values.globals.elasticApmSecretToken }}
- name: ELASTIC_APM_SECRET_TOKEN
value: {{ .Values.globals.elasticApmSecretToken | quote }}
{{ end }}
{{ if .Values.globals.elasticApmServerUrl }}
- name: ELASTIC_APM_SERVER_URL
value: {{ .Values.globals.elasticApmServerUrl | quote }}
{{ end }}
image: budibase/apps:{{ .Values.globals.appVersion }}
@ -142,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

@ -27,6 +27,8 @@ spec:
spec:
containers:
- env:
- name: BUDIBASE_ENVIRONMENT
value: {{ .Values.globals.budibaseEnv }}
- name: DEPLOYMENT_ENVIRONMENT
value: "kubernetes"
- name: CLUSTER_PORT
@ -125,6 +127,19 @@ spec:
value: {{ .Values.globals.google.secret | quote }}
- name: TENANT_FEATURE_FLAGS
value: {{ .Values.globals.tenantFeatureFlags | quote }}
{{ if .Values.globals.elasticApmEnabled }}
- name: ELASTIC_APM_ENABLED
value: {{ .Values.globals.elasticApmEnabled | quote }}
{{ end }}
{{ if .Values.globals.elasticApmSecretToken }}
- name: ELASTIC_APM_SECRET_TOKEN
value: {{ .Values.globals.elasticApmSecretToken | quote }}
{{ end }}
{{ if .Values.globals.elasticApmServerUrl }}
- name: ELASTIC_APM_SERVER_URL
value: {{ .Values.globals.elasticApmServerUrl | quote }}
{{ end }}
image: budibase/worker:{{ .Values.globals.appVersion }}
imagePullPolicy: Always
livenessProbe:
@ -136,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
@ -114,6 +101,10 @@ globals:
smtp:
enabled: false
# elasticApmEnabled:
# elasticApmSecretToken:
# elasticApmServerUrl:
services:
budibaseVersion: latest
dns: cluster.local
@ -121,15 +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
@ -143,6 +138,7 @@ services:
target: ""
# backup interval in seconds
interval: ""
resources: {}
redis:
enabled: true # disable if using external redis
@ -156,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
@ -172,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,7 +11,7 @@
"dependencies": {
"bulma": "^0.9.3",
"next": "12.1.0",
"node-fetch": "^3.2.2",
"node-fetch": "^3.2.10",
"node-sass": "^7.0.1",
"react": "17.0.2",
"react-dom": "17.0.2",

View File

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

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

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
@ -78,6 +81,7 @@ services:
image: budibase/proxy
environment:
- PROXY_RATE_LIMIT_WEBHOOKS_PER_SECOND=10
- PROXY_RATE_LIMIT_API_PER_SECOND=20
depends_on:
- minio-service
- worker-service

View File

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

View File

@ -15,7 +15,10 @@ http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
'"$http_user_agent" "$http_x_forwarded_for" '
'response_time=$upstream_response_time proxy_host=$proxy_host upstream_addr=$upstream_addr';
access_log /var/log/nginx/access.log main;
map $http_upgrade $connection_upgrade {
default "upgrade";
@ -81,6 +84,11 @@ http {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /vite {
proxy_pass http://{{ address }}:3000;
rewrite ^/vite(.*)$ /$1 break;
}
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

View File

@ -11,7 +11,7 @@ events {
http {
# rate limiting
limit_req_status 429;
limit_req_zone $binary_remote_addr zone=ratelimit:10m rate=20r/s;
limit_req_zone $binary_remote_addr zone=ratelimit:10m rate=${PROXY_RATE_LIMIT_API_PER_SECOND}r/s;
limit_req_zone $binary_remote_addr zone=webhooks:10m rate=${PROXY_RATE_LIMIT_WEBHOOKS_PER_SECOND}r/s;
include /etc/nginx/mime.types;
@ -33,7 +33,10 @@ http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
'"$http_user_agent" "$http_x_forwarded_for" '
'response_time=$upstream_response_time proxy_host=$proxy_host upstream_addr=$upstream_addr';
access_log /var/log/nginx/access.log main;
map $http_upgrade $connection_upgrade {
default "upgrade";

View File

@ -10,4 +10,5 @@ COPY .generated-nginx.prod.conf /etc/nginx/templates/nginx.conf.template
COPY error.html /usr/share/nginx/html/error.html
# Default environment
ENV PROXY_RATE_LIMIT_WEBHOOKS_PER_SECOND=10
ENV PROXY_RATE_LIMIT_WEBHOOKS_PER_SECOND=10
ENV PROXY_RATE_LIMIT_API_PER_SECOND=20

View File

@ -1,5 +1,5 @@
{
"version": "1.2.44-alpha.2",
"version": "1.3.12-alpha.3",
"npmClient": "yarn",
"packages": [
"packages/*"

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/backend-core",
"version": "1.2.44-alpha.2",
"version": "1.3.12-alpha.3",
"description": "Budibase backend core libraries used in server and worker",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
@ -20,7 +20,8 @@
"test:watch": "jest --watchAll"
},
"dependencies": {
"@budibase/types": "1.2.44-alpha.2",
"@budibase/types": "1.3.12-alpha.3",
"@shopify/jest-koa-mocks": "5.0.1",
"@techpass/passport-openidconnect": "0.3.2",
"aws-sdk": "2.1030.0",
"bcrypt": "5.0.1",
@ -60,7 +61,6 @@
]
},
"devDependencies": {
"@shopify/jest-koa-mocks": "3.1.5",
"@types/jest": "27.5.1",
"@types/koa": "2.0.52",
"@types/lodash": "4.14.180",

View File

@ -1,11 +1,11 @@
const passport = require("koa-passport")
const LocalStrategy = require("passport-local").Strategy
const JwtStrategy = require("passport-jwt").Strategy
const { getGlobalDB } = require("./tenancy")
import { getGlobalDB } from "./tenancy"
const refresh = require("passport-oauth2-refresh")
const { Configs } = require("./constants")
const { getScopedConfig } = require("./db/utils")
const {
import { Configs } from "./constants"
import { getScopedConfig } from "./db/utils"
import {
jwt,
local,
authenticated,
@ -13,7 +13,6 @@ const {
oidc,
auditLog,
tenancy,
appTenancy,
authError,
ssoCallbackUrl,
csrf,
@ -22,32 +21,36 @@ const {
builderOnly,
builderOrAdmin,
joiValidator,
} = require("./middleware")
const { invalidateUser } = require("./cache/user")
} from "./middleware"
import { invalidateUser } from "./cache/user"
import { User } from "@budibase/types"
// Strategies
passport.use(new LocalStrategy(local.options, local.authenticate))
passport.use(new JwtStrategy(jwt.options, jwt.authenticate))
passport.serializeUser((user, done) => done(null, user))
passport.serializeUser((user: User, done: any) => done(null, user))
passport.deserializeUser(async (user, done) => {
passport.deserializeUser(async (user: User, done: any) => {
const db = getGlobalDB()
try {
const user = await db.get(user._id)
return done(null, user)
const dbUser = await db.get(user._id)
return done(null, dbUser)
} catch (err) {
console.error(`User not found`, err)
return done(null, false, { message: "User not found" })
}
})
async function refreshOIDCAccessToken(db, chosenConfig, refreshToken) {
async function refreshOIDCAccessToken(
db: any,
chosenConfig: any,
refreshToken: string
) {
const callbackUrl = await oidc.getCallbackUrl(db, chosenConfig)
let enrichedConfig
let strategy
let enrichedConfig: any
let strategy: any
try {
enrichedConfig = await oidc.fetchStrategyConfig(chosenConfig, callbackUrl)
@ -70,22 +73,28 @@ async function refreshOIDCAccessToken(db, chosenConfig, refreshToken) {
refresh.requestNewAccessToken(
Configs.OIDC,
refreshToken,
(err, accessToken, refreshToken, params) => {
(err: any, accessToken: string, refreshToken: any, params: any) => {
resolve({ err, accessToken, refreshToken, params })
}
)
})
}
async function refreshGoogleAccessToken(db, config, refreshToken) {
async function refreshGoogleAccessToken(
db: any,
config: any,
refreshToken: any
) {
let callbackUrl = await google.getCallbackUrl(db, config)
let strategy
try {
strategy = await google.strategyFactory(config, callbackUrl)
} catch (err) {
} catch (err: any) {
console.error(err)
throw new Error("Error constructing OIDC refresh strategy", err)
throw new Error(
`Error constructing OIDC refresh strategy: message=${err.message}`
)
}
refresh.use(strategy)
@ -94,14 +103,18 @@ async function refreshGoogleAccessToken(db, config, refreshToken) {
refresh.requestNewAccessToken(
Configs.GOOGLE,
refreshToken,
(err, accessToken, refreshToken, params) => {
(err: any, accessToken: string, refreshToken: string, params: any) => {
resolve({ err, accessToken, refreshToken, params })
}
)
})
}
async function refreshOAuthToken(refreshToken, configType, configId) {
async function refreshOAuthToken(
refreshToken: string,
configType: string,
configId: string
) {
const db = getGlobalDB()
const config = await getScopedConfig(db, {
@ -113,7 +126,7 @@ async function refreshOAuthToken(refreshToken, configType, configId) {
let refreshResponse
if (configType === Configs.OIDC) {
// configId - retrieved from cookie.
chosenConfig = config.configs.filter(c => c.uuid === configId)[0]
chosenConfig = config.configs.filter((c: any) => c.uuid === configId)[0]
if (!chosenConfig) {
throw new Error("Invalid OIDC configuration")
}
@ -134,7 +147,7 @@ async function refreshOAuthToken(refreshToken, configType, configId) {
return refreshResponse
}
async function updateUserOAuth(userId, oAuthConfig) {
async function updateUserOAuth(userId: string, oAuthConfig: any) {
const details = {
accessToken: oAuthConfig.accessToken,
refreshToken: oAuthConfig.refreshToken,
@ -162,14 +175,13 @@ async function updateUserOAuth(userId, oAuthConfig) {
}
}
module.exports = {
export = {
buildAuthMiddleware: authenticated,
passport,
google,
oidc,
jwt: require("jsonwebtoken"),
buildTenancyMiddleware: tenancy,
buildAppTenancyMiddleware: appTenancy,
auditLog,
authError,
buildCsrfMiddleware: csrf,

View File

@ -18,6 +18,7 @@ export enum ViewName {
LINK = "by_link",
ROUTING = "screen_routes",
AUTOMATION_LOGS = "automation_logs",
ACCOUNT_BY_EMAIL = "account_by_email",
}
export const DeprecatedViews = {
@ -41,6 +42,7 @@ export enum DocumentType {
MIGRATIONS = "migrations",
DEV_INFO = "devinfo",
AUTOMATION_LOG = "log_au",
ACCOUNT_METADATA = "acc_metadata",
}
export const StaticDatabases = {

View File

@ -5,6 +5,8 @@ const {
SEPARATOR,
} = require("./utils")
const { getGlobalDB } = require("../tenancy")
const { StaticDatabases } = require("./constants")
const { doWithDB } = require("./")
const DESIGN_DB = "_design/database"
@ -56,6 +58,31 @@ exports.createNewUserEmailView = async () => {
await db.put(designDoc)
}
exports.createAccountEmailView = async () => {
await doWithDB(StaticDatabases.PLATFORM_INFO.name, async db => {
let designDoc
try {
designDoc = await db.get(DESIGN_DB)
} catch (err) {
// no design doc, make one
designDoc = DesignDoc()
}
const view = {
// if using variables in a map function need to inject them before use
map: `function(doc) {
if (doc._id.startsWith("${DocumentType.ACCOUNT_METADATA}${SEPARATOR}")) {
emit(doc.email.toLowerCase(), doc._id)
}
}`,
}
designDoc.views = {
...designDoc.views,
[ViewName.ACCOUNT_BY_EMAIL]: view,
}
await db.put(designDoc)
})
}
exports.createUserAppView = async () => {
const db = getGlobalDB()
let designDoc
@ -128,6 +155,39 @@ exports.createUserBuildersView = async () => {
await db.put(designDoc)
}
exports.queryView = async (viewName, params, db, CreateFuncByName) => {
try {
let response = (await db.query(`database/${viewName}`, params)).rows
response = response.map(resp =>
params.include_docs ? resp.doc : resp.value
)
if (params.arrayResponse) {
return response
} else {
return response.length <= 1 ? response[0] : response
}
} catch (err) {
if (err != null && err.name === "not_found") {
const createFunc = CreateFuncByName[viewName]
await removeDeprecated(db, viewName)
await createFunc()
return exports.queryView(viewName, params, db, CreateFuncByName)
} else {
throw err
}
}
}
exports.queryPlatformView = async (viewName, params) => {
const CreateFuncByName = {
[ViewName.ACCOUNT_BY_EMAIL]: exports.createAccountEmailView,
}
return doWithDB(StaticDatabases.PLATFORM_INFO.name, async db => {
return exports.queryView(viewName, params, db, CreateFuncByName)
})
}
exports.queryGlobalView = async (viewName, params, db = null) => {
const CreateFuncByName = {
[ViewName.USER_BY_EMAIL]: exports.createNewUserEmailView,
@ -139,20 +199,5 @@ exports.queryGlobalView = async (viewName, params, db = null) => {
if (!db) {
db = getGlobalDB()
}
try {
let response = (await db.query(`database/${viewName}`, params)).rows
response = response.map(resp =>
params.include_docs ? resp.doc : resp.value
)
return response.length <= 1 ? response[0] : response
} catch (err) {
if (err != null && err.name === "not_found") {
const createFunc = CreateFuncByName[viewName]
await removeDeprecated(db, viewName)
await createFunc()
return exports.queryGlobalView(viewName, params)
} else {
throw err
}
}
return exports.queryView(viewName, params, db, CreateFuncByName)
}

View File

@ -50,6 +50,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

@ -8,4 +8,5 @@ import { processors } from "./processors"
export const shutdown = () => {
processors.shutdown()
console.log("Events shutdown")
}

View File

@ -17,6 +17,7 @@ import constants from "./constants"
import * as dbConstants from "./db/constants"
import logging from "./logging"
import pino from "./pino"
import * as middleware from "./middleware"
// mimic the outer package exports
import * as db from "./pkg/db"
@ -57,6 +58,7 @@ const core = {
roles,
...pino,
...errorClasses,
middleware,
}
export = core

View File

@ -65,7 +65,7 @@ async function checkApiKey(apiKey: string, populateUser?: Function) {
* The tenancy modules should not be used here and it should be assumed that the tenancy context
* has not yet been populated.
*/
module.exports = (
export = (
noAuthPatterns = [],
opts: { publicAllowed: boolean; populateUser?: Function } = {
publicAllowed: false,

View File

@ -13,7 +13,8 @@ const adminOnly = require("./adminOnly")
const builderOrAdmin = require("./builderOrAdmin")
const builderOnly = require("./builderOnly")
const joiValidator = require("./joi-validator")
module.exports = {
const pkg = {
google,
oidc,
jwt,
@ -33,3 +34,5 @@ module.exports = {
builderOrAdmin,
joiValidator,
}
export = pkg

View File

@ -13,10 +13,13 @@ function validate(schema, property) {
params = ctx.request[property]
}
schema = schema.append({
createdAt: Joi.any().optional(),
updatedAt: Joi.any().optional(),
})
// not all schemas have the append property e.g. array schemas
if (schema.append) {
schema = schema.append({
createdAt: Joi.any().optional(),
updatedAt: Joi.any().optional(),
})
}
const { error } = schema.validate(params)
if (error) {

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.
@ -66,15 +70,13 @@ const PUBLIC_BUCKETS = [ObjectStoreBuckets.APPS, ObjectStoreBuckets.GLOBAL]
* @constructor
*/
export const ObjectStore = (bucket: any) => {
AWS.config.update({
accessKeyId: env.MINIO_ACCESS_KEY,
secretAccessKey: env.MINIO_SECRET_KEY,
region: env.AWS_REGION,
})
const config: any = {
s3ForcePathStyle: true,
signatureVersion: "v4",
apiVersion: "2006-03-01",
accessKeyId: env.MINIO_ACCESS_KEY,
secretAccessKey: env.MINIO_SECRET_KEY,
region: env.AWS_REGION,
}
if (bucket) {
config.params = {
@ -174,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),

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

@ -3,17 +3,27 @@ const { v4: uuidv4 } = require("uuid")
const { logWarn } = require("../logging")
const env = require("../environment")
interface Session {
key: string
userId: string
interface CreateSession {
sessionId: string
lastAccessedAt: string
createdAt: string
tenantId: string
csrfToken?: string
value: string
}
type SessionKey = { key: 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
}
// a week in seconds
const EXPIRY_SECONDS = 86400 * 7
@ -22,14 +32,14 @@ function makeSessionID(userId: string, sessionId: string) {
return `${userId}/${sessionId}`
}
export async function getSessionsForUser(userId: string) {
export async function getSessionsForUser(userId: string): Promise<Session[]> {
if (!userId) {
console.trace("Cannot get sessions for undefined userId")
return []
}
const client = await redis.getSessionClient()
const sessions = await client.scan(userId)
return sessions.map((session: Session) => session.value)
const sessions: ScannedSession[] = await client.scan(userId)
return sessions.map(session => session.value)
}
export async function invalidateSessions(
@ -39,33 +49,32 @@ export async function invalidateSessions(
try {
const reason = opts?.reason || "unknown"
let sessionIds: string[] = opts.sessionIds || []
let sessions: SessionKey
let sessionKeys: SessionKey[]
// If no sessionIds, get all the sessions for the user
if (sessionIds.length === 0) {
sessions = await getSessionsForUser(userId)
sessions.forEach(
(session: any) =>
(session.key = makeSessionID(session.userId, session.sessionId))
)
const sessions = await getSessionsForUser(userId)
sessionKeys = sessions.map(session => ({
key: makeSessionID(session.userId, session.sessionId),
}))
} else {
// use the passed array of sessionIds
sessionIds = Array.isArray(sessionIds) ? sessionIds : [sessionIds]
sessions = sessionIds.map((sessionId: string) => ({
sessionKeys = sessionIds.map(sessionId => ({
key: makeSessionID(userId, sessionId),
}))
}
if (sessions && sessions.length > 0) {
if (sessionKeys && sessionKeys.length > 0) {
const client = await redis.getSessionClient()
const promises = []
for (let session of sessions) {
promises.push(client.delete(session.key))
for (let sessionKey of sessionKeys) {
promises.push(client.delete(sessionKey.key))
}
if (!env.isTest()) {
logWarn(
`Invalidating sessions for ${userId} (reason: ${reason}) - ${sessions
.map(session => session.key)
`Invalidating sessions for ${userId} (reason: ${reason}) - ${sessionKeys
.map(sessionKey => sessionKey.key)
.join(", ")}`
)
}
@ -76,22 +85,26 @@ export async function invalidateSessions(
}
}
export async function createASession(userId: string, session: Session) {
export async function createASession(
userId: string,
createSession: CreateSession
) {
// invalidate all other sessions
await invalidateSessions(userId, { reason: "creation" })
const client = await redis.getSessionClient()
const sessionId = session.sessionId
if (!session.csrfToken) {
session.csrfToken = uuidv4()
}
session = {
...session,
const sessionId = createSession.sessionId
const csrfToken = createSession.csrfToken ? createSession.csrfToken : uuidv4()
const key = makeSessionID(userId, sessionId)
const session: Session = {
...createSession,
csrfToken,
createdAt: new Date().toISOString(),
lastAccessedAt: new Date().toISOString(),
userId,
}
await client.store(makeSessionID(userId, sessionId), session, EXPIRY_SECONDS)
await client.store(key, session, EXPIRY_SECONDS)
}
export async function updateSessionTTL(session: Session) {
@ -106,7 +119,10 @@ export async function endSession(userId: string, sessionId: string) {
await client.delete(makeSessionID(userId, sessionId))
}
export async function getSession(userId: string, sessionId: string) {
export async function getSession(
userId: string,
sessionId: string
): Promise<Session> {
if (!userId || !sessionId) {
throw new Error(`Invalid session details - ${userId} - ${sessionId}`)
}

View File

@ -11,7 +11,6 @@ const { UNICODE_MAX } = require("./db/constants")
* 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.
* @return {Promise<object|null>}
*/
exports.getGlobalUserByEmail = async email => {
if (email == null) {

View File

@ -0,0 +1,7 @@
export const getAccount = jest.fn()
export const getAccountByTenantId = jest.fn()
jest.mock("../../../src/cloud/accounts", () => ({
getAccount,
getAccountByTenantId,
}))

View File

@ -1,2 +0,0 @@
exports.MOCK_DATE = new Date("2020-01-01T00:00:00.000Z")
exports.MOCK_DATE_TIMESTAMP = 1577836800000

View File

@ -0,0 +1,2 @@
export const MOCK_DATE = new Date("2020-01-01T00:00:00.000Z")
export const MOCK_DATE_TIMESTAMP = 1577836800000

View File

@ -1,9 +0,0 @@
const posthog = require("./posthog")
const events = require("./events")
const date = require("./date")
module.exports = {
posthog,
date,
events,
}

View File

@ -0,0 +1,4 @@
import "./posthog"
import "./events"
export * as accounts from "./accounts"
export * as date from "./date"

View File

@ -543,13 +543,13 @@
semver "^7.3.5"
tar "^6.1.11"
"@shopify/jest-koa-mocks@3.1.5":
version "3.1.5"
resolved "https://registry.yarnpkg.com/@shopify/jest-koa-mocks/-/jest-koa-mocks-3.1.5.tgz#11f77ccfbcaf35cf5ee2c6108a286e61e6bea084"
integrity sha512-gQ3/7ELerv00TWO37AGFX5mT9CsFCS+3/UbKMuoIlKEU0QH2OX8BV9WBf/EKw7adCDNlxss0lqV6J8kf5pgr4A==
"@shopify/jest-koa-mocks@5.0.1":
version "5.0.1"
resolved "https://registry.yarnpkg.com/@shopify/jest-koa-mocks/-/jest-koa-mocks-5.0.1.tgz#fba490b6b7985fbb571eb9974897d396a3642e94"
integrity sha512-4YskS9q8+TEHNoyopmuoy2XyhInyqeOl7CF5ShJs19sm6m0EA/jGGvgf/osv2PeTfuf42/L2G9CzWUSg49yTSg==
dependencies:
koa "^2.13.4"
node-mocks-http "^1.5.8"
node-mocks-http "^1.11.0"
"@sideway/address@^4.1.3":
version "4.1.4"
@ -3914,7 +3914,7 @@ node-int64@^0.4.0:
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==
node-mocks-http@^1.5.8:
node-mocks-http@^1.11.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/node-mocks-http/-/node-mocks-http-1.11.0.tgz#defc0febf6b935f08245397d47534a8de592996e"
integrity sha512-jS/WzSOcKbOeGrcgKbenZeNhxUNnP36Yw11+hL4TTxQXErGfqYZ+MaYNNvhaTiGIJlzNSqgQkk9j8dSu1YWSuw==

View File

@ -1,7 +1,7 @@
{
"name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.",
"version": "1.2.44-alpha.2",
"version": "1.3.12-alpha.3",
"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.2.44-alpha.2",
"@budibase/string-templates": "1.3.12-alpha.3",
"@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

@ -67,6 +67,13 @@
// If time only set date component to 2000-01-01
if (timeOnly) {
// Classic flackpickr causing issues.
// When selecting a value for the first time for a "time only" field,
// the time is always offset by 1 hour for some reason (regardless of time
// zone) so we need to correct it.
if (!value && newValue) {
newValue = new Date(dates[0].getTime() + 60 * 60 * 1000).toISOString()
}
newValue = `2000-01-01T${newValue.split("T")[1]}`
}

View File

@ -139,7 +139,13 @@
<div class="title">
<div class="filename">
{#if selectedUrl}
<Link href={selectedUrl}>{selectedImage.name}</Link>
<Link
target="_blank"
download={selectedImage.name}
href={selectedUrl}
>
{selectedImage.name}
</Link>
{:else}
{selectedImage.name}
{/if}

View File

@ -10,6 +10,7 @@
export let disabled = false
export let getOptionLabel = option => option
export let getOptionValue = option => option
export let getOptionTitle = option => option
const dispatch = createEventDispatcher()
const onChange = e => dispatch("change", e.target.value)
@ -19,7 +20,7 @@
{#if options && Array.isArray(options)}
{#each options as option}
<div
title={getOptionLabel(option)}
title={getOptionTitle(option)}
class="spectrum-Radio spectrum-FieldGroup-item spectrum-Radio--emphasized"
class:is-invalid={!!error}
>

View File

@ -12,6 +12,7 @@
export let direction = "vertical"
export let getOptionLabel = option => extractProperty(option, "label")
export let getOptionValue = option => extractProperty(option, "value")
export let getOptionTitle = option => extractProperty(option, "label")
const dispatch = createEventDispatcher()
const onChange = e => {
@ -35,6 +36,7 @@
{direction}
{getOptionLabel}
{getOptionValue}
{getOptionTitle}
on:change={onChange}
/>
</Field>

View File

@ -8,12 +8,14 @@
export let secondary = false
export let overBackground = false
export let target
export let download
</script>
<a
on:click
{href}
{target}
{download}
class:spectrum-Link--primary={primary}
class:spectrum-Link--secondary={secondary}
class:spectrum-Link--overBackground={overBackground}

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

@ -15,14 +15,24 @@
{#each attachments as attachment}
{#if isImage(attachment.extension)}
<Link quiet target="_blank" href={attachment.url}>
<Link
quiet
target="_blank"
download={attachment.name}
href={attachment.url}
>
<div class="center" title={attachment.name}>
<img src={attachment.url} alt={attachment.extension} />
</div>
</Link>
{:else}
<div class="file" title={attachment.name}>
<Link quiet target="_blank" href={attachment.url}>
<Link
quiet
target="_blank"
download={attachment.name}
href={attachment.url}
>
{attachment.extension}
</Link>
</div>

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

@ -102,7 +102,7 @@ filterTests(['all'], () => {
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 6000 })
cy.wait(500)
cy.get(interact.APP_TABLE_STATUS, { timeout: 1000 }).eq(0).contains("Unpublished")
cy.get(interact.APP_TABLE_STATUS, { timeout: 10000 }).eq(0).contains("Unpublished")
})
})

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

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

@ -175,7 +175,10 @@ filterTests(["all"], () => {
cy.get("@query").its("response.statusCode").should("eq", 200)
cy.get("@query").its("response.body").should("not.be.empty")
// Save query
cy.intercept("POST", "**/queries").as("saveQuery")
cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
cy.wait("@saveQuery")
cy.get("@saveQuery").its("response.statusCode").should("eq", 200)
cy.get(".nav-item").should("contain", queryName)
})

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")
@ -252,7 +252,8 @@ filterTests(["all"], () => {
.contains("Delete Query")
.click({ force: true })
// Confirm deletion
cy.reload({ timeout: 5000 })
cy.reload()
cy.get(".nav-item", { timeout: 30000 }).contains(datasource).click({ force: true })
cy.get(".nav-item", { timeout: 1000 }).should("not.contain", queryRename)
})

View File

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

@ -48,6 +48,7 @@ filterTests(['smoke', 'all'], () => {
cy.get(interact.AREA_LABEL_REVERT).click({ force: true })
})
cy.get(interact.SPECTRUM_DIALOG_GRID).within(() => {
cy.get("input").type("Cypress Tests")
// Click Revert
cy.get(interact.SPECTRUM_BUTTON).contains("Revert").click({ force: true })
cy.wait(2000) // Wait for app to finish reverting

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")
})
@ -448,17 +451,14 @@ Cypress.Commands.add("createTable", (tableName, initialTable) => {
.contains("Continue")
.click({ force: true })
})
cy.get(".spectrum-Modal", { timeout: 10000 }).should(
"not.contain",
"Add data source"
)
cy.get(".spectrum-Modal").contains("Create Table", { timeout: 10000 })
cy.get(".spectrum-Modal", { timeout: 2000 }).within(() => {
cy.get("input", { timeout: 2000 }).first().type(tableName).blur()
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
)
@ -637,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 })
@ -742,8 +744,15 @@ Cypress.Commands.add("deleteAllScreens", () => {
Cypress.Commands.add("navigateToFrontend", () => {
// Clicks on Design tab and then the Home nav item
cy.wait(500)
cy.intercept("**/preview").as("preview")
cy.contains("Design").click()
cy.get(".spectrum-Search", { timeout: 2000 }).type("/")
cy.wait("@preview")
cy.get("@preview").then(res => {
if (res.statusCode != 200) {
cy.reload()
}
})
cy.get(".spectrum-Search", { timeout: 20000 }).type("/")
cy.get(".nav-item", { timeout: 2000 }).contains("home").click({ force: true })
})
@ -908,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.2.44-alpha.2",
"version": "1.3.12-alpha.3",
"license": "GPL-3.0",
"private": true,
"scripts": {
@ -69,10 +69,10 @@
}
},
"dependencies": {
"@budibase/bbui": "1.2.44-alpha.2",
"@budibase/client": "1.2.44-alpha.2",
"@budibase/frontend-core": "1.2.44-alpha.2",
"@budibase/string-templates": "1.2.44-alpha.2",
"@budibase/bbui": "1.3.12-alpha.3",
"@budibase/client": "1.3.12-alpha.3",
"@budibase/frontend-core": "1.3.12-alpha.3",
"@budibase/string-templates": "1.3.12-alpha.3",
"@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1",
@ -96,7 +96,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",
@ -118,7 +118,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

@ -299,7 +299,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
@ -359,6 +362,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,
},
})
})
})
@ -385,6 +394,9 @@ const getUserBindings = () => {
// datasource options, based on bindable properties
fieldSchema,
providerId: "user",
category: "Current User",
icon: "User",
display: fieldSchema,
})
})
return bindings
@ -401,11 +413,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
@ -429,6 +447,8 @@ const getSelectedRowsBindings = asset => {
"selectedRows"
)}`,
readableBinding: `${table._instanceName}.Selected rows`,
category: "Selected rows",
icon: "ViewRow",
}))
)
@ -460,6 +480,9 @@ const getStateBindings = () => {
type: "context",
runtimeBinding: `${safeState}.${makePropSafe(key)}`,
readableBinding: `State.${key}`,
category: "State",
icon: "AutomatedSegment",
display: { name: key },
}))
}
return bindings
@ -482,11 +505,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])
}
@ -497,6 +526,9 @@ const getRoleBindings = () => {
type: "context",
runtimeBinding: `trim "${role._id}"`,
readableBinding: `Role.${role.name}`,
category: "Role",
icon: "UserGroup",
display: { type: "string", name: role.name },
}
})
}
@ -518,6 +550,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) {
@ -527,6 +560,8 @@ export const getEventContextBindings = (
runtimeBinding: `${makePropSafe("eventContext")}.${makePropSafe(
contextEntry.key
)}`,
category: component._instanceName,
icon: def.icon,
})
})
}
@ -548,6 +583,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

@ -19,7 +19,6 @@ import {
makeComponentUnique,
} from "../componentUtils"
import { Helpers } from "@budibase/bbui"
import { DefaultAppTheme, LAYOUT_NAMES } from "../../constants"
import { Utils } from "@budibase/frontend-core"
const INITIAL_FRONTEND_STATE = {
@ -91,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,
@ -117,6 +124,7 @@ export const getFrontendStore = () => {
version: application.version,
revertableVersion: application.revertableVersion,
navigation: application.navigation || {},
usedPlugins: application.usedPlugins || [],
}))
// Initialise backend stores
@ -125,35 +133,6 @@ export const getFrontendStore = () => {
await integrations.init()
await queries.init()
await tables.init()
// Add navigation settings to old apps
if (!application.navigation) {
const layout = layouts.find(x => x._id === LAYOUT_NAMES.MASTER.PRIVATE)
const customTheme = application.customTheme
let navigationSettings = {
navigation: "Top",
title: application.name,
navWidth: "Large",
navBackground:
customTheme?.navBackground || DefaultAppTheme.navBackground,
navTextColor:
customTheme?.navTextColor || DefaultAppTheme.navTextColor,
}
if (layout) {
navigationSettings.hideLogo = layout.props.hideLogo
navigationSettings.hideTitle = layout.props.hideTitle
navigationSettings.title = layout.props.title || application.name
navigationSettings.logoUrl = layout.props.logoUrl
navigationSettings.links = layout.props.links
navigationSettings.navigation = layout.props.navigation || "Top"
navigationSettings.sticky = layout.props.sticky
navigationSettings.navWidth = layout.props.width || "Large"
if (navigationSettings.navigation === "None") {
navigationSettings.navigation = "Top"
}
}
await store.actions.navigation.save(navigationSettings)
}
},
theme: {
save: async theme => {
@ -219,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)
@ -240,6 +228,9 @@ export const getFrontendStore = () => {
// Update routes
state.routes = routesResponse.routes
// Update used plugins
state.usedPlugins = usedPlugins
return state
})
return savedScreen
@ -398,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

@ -23,7 +23,7 @@
</script>
<div class="automations-list">
{#each $automationStore.automations as automation, idx}
{#each $automationStore.automations.sort(aut => aut.name) as automation, idx}
<NavItem
border={idx > 0}
icon="ShareAndroid"

View File

@ -14,7 +14,7 @@
$: {
let fields = {}
for (const [key, type] of Object.entries(block?.inputs?.fields)) {
for (const [key, type] of Object.entries(block?.inputs?.fields ?? {})) {
fields = {
...fields,
[key]: {

View File

@ -467,6 +467,7 @@
options={relationshipOptions}
getOptionLabel={option => option.name}
getOptionValue={option => option.value}
getOptionTitle={option => option.alt}
/>
{/if}
<Input

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

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

@ -16,6 +16,7 @@
export let scrollable = false
export let highlighted = false
export let rightAlignIcon = false
export let id
const scrollApi = getContext("scroll")
const dispatch = createEventDispatcher()
@ -58,6 +59,7 @@
on:click={onClick}
ondragover="return false"
ondragenter="return false"
{id}
>
<div class="nav-item-content" bind:this={contentRef}>
{#if withArrow}

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>
{#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);

View File

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

View File

@ -31,7 +31,9 @@
class="add-button"
data-cy={`new-${isExternal ? "datasource" : "table"}`}
>
<Icon hoverable name="AddCircle" on:click={modal.show} />
{#if modal}
<Icon hoverable name="AddCircle" on:click={modal.show} />
{/if}
</div>
</div>
<div class="content">

View File

@ -85,6 +85,7 @@
? [$store.componentToPaste?._id]
: [],
isBudibaseEvent: true,
usedPlugins: $store.usedPlugins,
}
// Refresh the preview when required
@ -183,7 +184,7 @@
$goto("./navigation")
}
} else if (type === "request-add-component") {
$goto(`./components/${$selectedComponent?._id}/new`)
toggleAddComponent()
} else if (type === "highlight-setting") {
store.actions.settings.highlight(data.setting)
@ -227,9 +228,8 @@
if (isAddingComponent) {
$goto(`../${$selectedScreen._id}/components/${$selectedComponent?._id}`)
} else {
$goto(
`../${$selectedScreen._id}/components/${$selectedComponent?._id}/new`
)
const id = $selectedComponent?._id || $selectedScreen?.props?._id
$goto(`../${$selectedScreen._id}/components/${id}/new`)
}
}

View File

@ -2,10 +2,20 @@
import { store } from "builderStore"
import { ActionMenu, MenuItem, Icon } from "@budibase/bbui"
export let component
$: noPaste = !$store.componentToPaste
const keyboardEvent = (key, ctrlKey = false) => {
document.dispatchEvent(new KeyboardEvent("keydown", { key, ctrlKey }))
document.dispatchEvent(
new CustomEvent("component-menu", {
detail: {
key,
ctrlKey,
id: component?._id,
},
})
)
}
</script>

View File

@ -0,0 +1,126 @@
<script>
import { onMount } from "svelte"
import { selectedComponent, selectedScreen, store } from "builderStore"
import { findComponent } from "builderStore/componentUtils"
import { goto, isActive } from "@roxi/routify"
import { notifications } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
let confirmDeleteDialog
let componentToDelete
const keyHandlers = {
["^ArrowUp"]: async component => {
await store.actions.components.moveUp(component)
},
["^ArrowDown"]: async component => {
await store.actions.components.moveDown(component)
},
["^c"]: component => {
store.actions.components.copy(component, false)
},
["^x"]: component => {
store.actions.components.copy(component, true)
},
["^v"]: async component => {
await store.actions.components.paste(component, "inside")
},
["^d"]: async component => {
store.actions.components.copy(component)
await store.actions.components.paste(component, "below")
},
["^Enter"]: () => {
$goto("./new")
},
["Delete"]: component => {
// Don't show confirmation for the screen itself
if (component?._id === $selectedScreen.props._id) {
return false
}
componentToDelete = component
confirmDeleteDialog.show()
},
["ArrowUp"]: () => {
store.actions.components.selectPrevious()
},
["ArrowDown"]: () => {
store.actions.components.selectNext()
},
["Escape"]: () => {
if (!$isActive("/new")) {
return false
}
$goto("./")
},
}
const handleKeyAction = async (event, component, key, ctrlKey = false) => {
if (!component || !key) {
return false
}
try {
// Delete and backspace are the same
if (key === "Backspace") {
key = "Delete"
}
// Prefix key with a caret for ctrl modifier
if (ctrlKey) {
key = "^" + key
}
const handler = keyHandlers[key]
if (!handler) {
return false
} else if (event) {
event.preventDefault()
event.stopPropagation()
}
return handler(component)
} catch (error) {
console.error(error)
notifications.error("Error handling key press")
}
}
const handleKeyPress = async e => {
// Ignore repeating events
if (e.repeat) {
return
}
// Ignore events when typing
const activeTag = document.activeElement?.tagName.toLowerCase()
if (["input", "textarea"].indexOf(activeTag) !== -1 && e.key !== "Escape") {
return
}
// Key events are always for the selected component
return await handleKeyAction(
e,
$selectedComponent,
e.key,
e.ctrlKey || e.metaKey
)
}
const handleComponentMenu = async e => {
// Menu events can be for any component
const { id, key, ctrlKey } = e.detail
const component = findComponent($selectedScreen.props, id)
return await handleKeyAction(null, component, key, ctrlKey)
}
onMount(() => {
document.addEventListener("keydown", handleKeyPress)
document.addEventListener("component-menu", handleComponentMenu)
return () => {
document.removeEventListener("keydown", handleKeyPress)
document.removeEventListener("component-menu", handleComponentMenu)
}
})
</script>
<ConfirmDialog
bind:this={confirmDeleteDialog}
title="Confirm Deletion"
body={`Are you sure you want to delete "${componentToDelete?._instanceName}"?`}
okText="Delete Component"
onOk={() => store.actions.components.delete(componentToDelete)}
/>

View File

@ -2,62 +2,15 @@
import Panel from "components/design/Panel.svelte"
import ComponentTree from "./ComponentTree.svelte"
import { dndStore } from "./dndStore.js"
import { goto, isActive } from "@roxi/routify"
import { store, selectedScreen, selectedComponent } from "builderStore"
import { goto } from "@roxi/routify"
import { store, selectedScreen } from "builderStore"
import NavItem from "components/common/NavItem.svelte"
import ScreenslotDropdownMenu from "./ScreenslotDropdownMenu.svelte"
import { setContext, onMount } from "svelte"
import { get } from "svelte/store"
import DNDPositionIndicator from "./DNDPositionIndicator.svelte"
import { DropPosition } from "./dndStore"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { notifications, Button } from "@budibase/bbui"
let scrollRef
let confirmDeleteDialog
const scrollTo = bounds => {
if (!bounds) {
return
}
const sidebarWidth = 259
const navItemHeight = 32
const { scrollLeft, scrollTop, offsetHeight } = scrollRef
let scrollBounds = scrollRef.getBoundingClientRect()
let newOffsets = {}
// Calculate left offset
const offsetX = bounds.left + bounds.width + scrollLeft - 36
if (offsetX > sidebarWidth) {
newOffsets.left = offsetX - sidebarWidth
} else {
newOffsets.left = 0
}
if (newOffsets.left === scrollLeft) {
delete newOffsets.left
}
// Calculate top offset
const offsetY = bounds.top - scrollBounds?.top + scrollTop
if (offsetY > scrollTop + offsetHeight - 2 * navItemHeight) {
newOffsets.top = offsetY - offsetHeight + 2 * navItemHeight
} else if (offsetY < scrollTop + navItemHeight) {
newOffsets.top = offsetY - navItemHeight
} else {
delete newOffsets.top
}
// Skip if offset is unchanged
if (newOffsets.left == null && newOffsets.top == null) {
return
}
// Smoothly scroll to the offset
scrollRef.scroll({
...newOffsets,
behavior: "smooth",
})
}
import ComponentKeyHandler from "./ComponentKeyHandler.svelte"
import ComponentScrollWrapper from "./ComponentScrollWrapper.svelte"
const onDrop = async () => {
try {
@ -67,95 +20,15 @@
notifications.error("Error saving component")
}
}
// Set scroll context so components can invoke scrolling when selected
setContext("scroll", {
scrollTo,
})
const deleteComponent = async () => {
await store.actions.components.delete(get(selectedComponent))
}
const handleKeyPress = async e => {
// Ignore repeating events
if (e.repeat) {
return
}
// Ignore events when typing
const activeTag = document.activeElement?.tagName.toLowerCase()
if (["input", "textarea"].indexOf(activeTag) !== -1 && e.key !== "Escape") {
return
}
const component = get(selectedComponent)
try {
if (e.ctrlKey || e.metaKey) {
if (e.key === "ArrowUp") {
e.preventDefault()
await store.actions.components.moveUp(component)
} else if (e.key === "ArrowDown") {
e.preventDefault()
await store.actions.components.moveDown(component)
} else if (e.key === "c") {
e.preventDefault()
await store.actions.components.copy(component, false)
} else if (e.key === "x") {
e.preventDefault()
store.actions.components.copy(component, true)
} else if (e.key === "v") {
e.preventDefault()
await store.actions.components.paste(component, "inside")
} else if (e.key === "d") {
e.preventDefault()
await store.actions.components.copy(component)
await store.actions.components.paste(component, "below")
} else if (e.key === "Enter") {
e.preventDefault()
$goto("./new")
}
} else if (e.key === "Backspace" || e.key === "Delete") {
// Don't show confirmation for the screen itself
if (component._id === get(selectedScreen).props._id) {
return
}
e.preventDefault()
confirmDeleteDialog.show()
} else if (e.key === "ArrowUp") {
e.preventDefault()
await store.actions.components.selectPrevious()
} else if (e.key === "ArrowDown") {
e.preventDefault()
await store.actions.components.selectNext()
} else if (e.key === "Escape" && $isActive("./new")) {
e.preventDefault()
$goto("./")
}
} catch (error) {
console.log(error)
notifications.error("Error handling key press")
}
}
onMount(() => {
document.addEventListener("keydown", handleKeyPress)
return () => {
document.removeEventListener("keydown", handleKeyPress)
}
})
</script>
<Panel title="Components" showExpandIcon borderRight>
<div class="add-component">
<Button on:click={() => $goto("./new")} cta>Add component</Button>
</div>
<div class="nav-items-container" bind:this={scrollRef}>
<ComponentScrollWrapper>
<ul>
<li
on:click={() => {
$store.selectedComponentId = $selectedScreen?.props._id
}}
id={`component-${$selectedScreen?.props._id}`}
>
<li>
<NavItem
text="Screen"
indentLevel={0}
@ -164,6 +37,10 @@
scrollable
icon="WebPage"
on:drop={onDrop}
on:click={() => {
$store.selectedComponentId = $selectedScreen?.props._id
}}
id={`component-${$selectedScreen?.props._id}`}
>
<ScreenslotDropdownMenu component={$selectedScreen?.props} />
</NavItem>
@ -187,15 +64,9 @@
{/if}
</li>
</ul>
</div>
</ComponentScrollWrapper>
</Panel>
<ConfirmDialog
bind:this={confirmDeleteDialog}
title="Confirm Deletion"
body={`Are you sure you want to delete "${$selectedComponent?._instanceName}"?`}
okText="Delete Component"
onOk={deleteComponent}
/>
<ComponentKeyHandler />
<style>
.add-component {
@ -205,12 +76,6 @@
flex-direction: column;
align-items: stretch;
}
.nav-items-container {
padding: var(--spacing-xl) 0;
flex: 1 1 auto;
overflow: auto;
height: 0;
}
ul {
list-style: none;
padding-left: 0;

View File

@ -0,0 +1,82 @@
<script>
import { setContext } from "svelte"
import { dndStore } from "./dndStore"
import { notifications } from "@budibase/bbui"
let scrollRef
const scrollTo = bounds => {
if (!bounds) {
return
}
const sidebarWidth = 259
const navItemHeight = 32
const { scrollLeft, scrollTop, offsetHeight } = scrollRef
let scrollBounds = scrollRef.getBoundingClientRect()
let newOffsets = {}
// Calculate left offset
const offsetX = bounds.left + bounds.width + scrollLeft - 36
if (offsetX > sidebarWidth) {
newOffsets.left = offsetX - sidebarWidth
} else {
newOffsets.left = 0
}
if (newOffsets.left === scrollLeft) {
delete newOffsets.left
}
// Calculate top offset
const offsetY = bounds.top - scrollBounds?.top + scrollTop
if (offsetY > scrollTop + offsetHeight - 2 * navItemHeight) {
newOffsets.top = offsetY - offsetHeight + 2 * navItemHeight
} else if (offsetY < scrollTop + navItemHeight) {
newOffsets.top = offsetY - navItemHeight
} else {
delete newOffsets.top
}
// Skip if offset is unchanged
if (newOffsets.left == null && newOffsets.top == null) {
return
}
// Smoothly scroll to the offset
scrollRef.scroll({
...newOffsets,
behavior: "smooth",
})
}
// Set scroll context so components can invoke scrolling when selected
setContext("scroll", {
scrollTo,
})
const onDrop = async () => {
try {
await dndStore.actions.drop()
} catch (error) {
console.error(error)
notifications.error("Error saving component")
}
}
</script>
<div
bind:this={scrollRef}
on:drop={onDrop}
ondragover="return false"
ondragenter="return false"
>
<slot />
</div>
<style>
div {
padding: var(--spacing-xl) 0;
flex: 1 1 auto;
overflow: auto;
height: 0;
}
</style>

View File

@ -68,7 +68,8 @@
closedNodes = closedNodes
}
const onDrop = async () => {
const onDrop = async e => {
e.stopPropagation()
try {
await dndStore.actions.drop()
} catch (error) {

View File

@ -28,7 +28,7 @@
{#if $selectedComponent}
{#key $selectedComponent._id}
<Panel {title} icon={componentDefinition.icon} borderLeft>
<Panel {title} icon={componentDefinition?.icon} borderLeft>
<ComponentSettingsSection
{componentInstance}
{componentDefinition}

View File

@ -24,7 +24,11 @@
$: currentDefinition = store.actions.components.getDefinition(
$selectedComponent?._component
)
$: enrichedStructure = enrichStructure(structure, $store.components)
$: enrichedStructure = enrichStructure(
structure,
$store.components,
$store.customComponents
)
$: filteredStructure = filterStructure(
enrichedStructure,
section,
@ -46,8 +50,25 @@
// Parses the structure in the manifest and returns an enriched structure with
// explicit categories
const enrichStructure = (structure, definitions) => {
const enrichStructure = (structure, definitions, customComponents) => {
let enrichedStructure = []
// Add custom components category
if (customComponents?.length) {
enrichedStructure.push({
name: "Plugins",
isCategory: true,
children: customComponents
.map(x => ({
...definitions[x],
name: definitions[x].friendlyName || definitions[x].name,
}))
.sort((a, b) => {
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1
}),
})
}
structure.forEach(item => {
if (typeof item === "string") {
const def = definitions[`@budibase/standard-components/${item}`]
@ -65,6 +86,7 @@
})
}
})
return enrichedStructure
}
@ -225,7 +247,7 @@
position: fixed;
right: 0;
z-index: 1;
height: 100%;
height: calc(100% - 60px);
display: flex;
flex-direction: row;
align-items: stretch;

View File

@ -44,7 +44,11 @@
]
}
function validateInput(email, index) {
function validateInput(input, index) {
if (input.email) {
input.email = input.email.trim()
}
const email = input.email
if (email) {
const res = emailValidator(email)
if (res === true) {
@ -61,7 +65,7 @@
const onConfirm = () => {
let valid = true
userData.forEach((input, index) => {
valid = validateInput(input.email, index) && valid
valid = validateInput(input, index) && valid
})
if (!valid) {
return false
@ -95,7 +99,7 @@
bind:dropdownValue={input.role}
options={Constants.BudibaseRoleOptions}
error={input.error}
on:blur={() => validateInput(input.email, index)}
on:blur={() => validateInput(input, index)}
/>
</div>
<div class="icon">

View File

@ -0,0 +1,73 @@
<script>
import { Body, ModalContent, Table } from "@budibase/bbui"
import { onMount } from "svelte"
export let userData
export let deleteUsersResponse
let successCount
let failureCount
let title
let unsuccessfulUsers
let message
const setTitle = () => {
if (successCount) {
title = `${successCount} users deleted`
} else {
title = "Oops!"
}
}
const setMessage = () => {
if (successCount) {
message = "However there was a problem deleting some users."
} else {
message = "There was a problem deleting some users."
}
}
const setUsers = () => {
unsuccessfulUsers = deleteUsersResponse.unsuccessful.map(user => {
return {
email: user.email,
reason: user.reason,
}
})
}
onMount(() => {
successCount = deleteUsersResponse.successful.length
failureCount = deleteUsersResponse.unsuccessful.length
setTitle()
setMessage()
setUsers()
})
const schema = {
email: {},
reason: {},
}
</script>
<ModalContent
size="M"
{title}
confirmText="Close"
showCloseIcon={false}
showCancelButton={false}
>
<Body size="XS">
{message}
</Body>
<Table
{schema}
data={unsuccessfulUsers}
allowEditColumns={false}
allowEditRows={false}
allowSelectRows={false}
/>
</ModalContent>
<style>
</style>

View File

@ -62,7 +62,7 @@
csvString = e.target.result
files = fileArray
userEmails = csvString.split("\n")
userEmails = csvString.split(/\r?\n/)
})
reader.readAsText(fileArray[0])
}

View File

@ -0,0 +1,75 @@
<script>
import { Body, ModalContent, Table } from "@budibase/bbui"
import { onMount } from "svelte"
export let inviteUsersResponse
let hasSuccess
let hasFailure
let title
let failureMessage
let unsuccessfulUsers
const setTitle = () => {
if (hasSuccess) {
title = "Users invited!"
} else if (hasFailure) {
title = "Oops!"
}
}
const setFailureMessage = () => {
if (hasSuccess) {
failureMessage = "However there was a problem inviting some users."
} else {
failureMessage = "There was a problem inviting users."
}
}
const setUsers = () => {
unsuccessfulUsers = inviteUsersResponse.unsuccessful.map(user => {
return {
email: user.email,
reason: user.reason,
}
})
}
onMount(() => {
hasSuccess = inviteUsersResponse.successful.length
hasFailure = inviteUsersResponse.unsuccessful.length
setTitle()
setFailureMessage()
setUsers()
})
const failedSchema = {
email: {},
reason: {},
}
</script>
<ModalContent showCancelButton={false} {title} confirmText="Done">
{#if hasSuccess}
<Body size="XS">
Your users should now receive an email invite to get access to their
Budibase account
</Body>
{/if}
{#if hasFailure}
<Body size="XS">
{failureMessage}
</Body>
<Table
schema={failedSchema}
data={unsuccessfulUsers}
allowEditColumns={false}
allowEditRows={false}
allowSelectRows={false}
/>
{/if}
</ModalContent>
<style>
</style>

View File

@ -2,24 +2,78 @@
import { Body, ModalContent, Table, Icon } from "@budibase/bbui"
import PasswordCopyRenderer from "./PasswordCopyRenderer.svelte"
import { parseToCsv } from "helpers/data/utils"
import { onMount } from "svelte"
export let userData
export let createUsersResponse
$: mappedData = userData.map(user => {
return {
email: user.email,
password: user.password,
let hasSuccess
let hasFailure
let title
let failureMessage
let userDataIndex
let successfulUsers
let unsuccessfulUsers
const setTitle = () => {
if (hasSuccess) {
title = "Users created!"
} else if (hasFailure) {
title = "Oops!"
}
}
const setFailureMessage = () => {
if (hasSuccess) {
failureMessage = "However there was a problem creating some users."
} else {
failureMessage = "There was a problem creating some users."
}
}
const setUsers = () => {
userDataIndex = userData.reduce((prev, current) => {
prev[current.email] = current
return prev
}, {})
successfulUsers = createUsersResponse.successful.map(user => {
return {
email: user.email,
password: userDataIndex[user.email].password,
}
})
unsuccessfulUsers = createUsersResponse.unsuccessful.map(user => {
return {
email: user.email,
reason: user.reason,
}
})
}
onMount(() => {
hasSuccess = createUsersResponse.successful.length
hasFailure = createUsersResponse.unsuccessful.length
setTitle()
setFailureMessage()
setUsers()
})
const schema = {
const successSchema = {
email: {},
password: {},
}
const failedSchema = {
email: {},
reason: {},
}
const downloadCsvFile = () => {
const fileName = "passwords.csv"
const content = parseToCsv(["email", "password"], mappedData)
const content = parseToCsv(["email", "password"], successfulUsers)
download(fileName, content)
}
@ -43,35 +97,51 @@
<ModalContent
size="M"
title="Accounts created!"
{title}
confirmText="Done"
showCancelButton={false}
cancelText="Cancel"
showCloseIcon={false}
>
<Body size="XS">
All your new users can be accessed through the autogenerated passwords. Take
note of these passwords or download the CSV file.
</Body>
{#if hasFailure}
<Body size="XS">
{failureMessage}
</Body>
<Table
schema={failedSchema}
data={unsuccessfulUsers}
allowEditColumns={false}
allowEditRows={false}
allowSelectRows={false}
/>
{/if}
{#if hasSuccess}
<Body size="XS">
All your new users can be accessed through the autogenerated passwords.
Take note of these passwords or download the CSV file.
</Body>
<div class="container" on:click={downloadCsvFile}>
<div class="inner">
<Icon name="Download" />
<div class="container" on:click={downloadCsvFile}>
<div class="inner">
<Icon name="Download" />
<div style="margin-left: var(--spacing-m)">
<Body size="XS">Passwords CSV</Body>
<div style="margin-left: var(--spacing-m)">
<Body size="XS">Passwords CSV</Body>
</div>
</div>
</div>
</div>
<Table
{schema}
data={mappedData}
allowEditColumns={false}
allowEditRows={false}
allowSelectRows={false}
customRenderers={[{ column: "password", component: PasswordCopyRenderer }]}
/>
<Table
schema={successSchema}
data={successfulUsers}
allowEditColumns={false}
allowEditRows={false}
allowSelectRows={false}
customRenderers={[
{ column: "password", component: PasswordCopyRenderer },
]}
/>
{/if}
</ModalContent>
<style>

View File

@ -7,7 +7,6 @@
Table,
Layout,
Modal,
ModalContent,
Search,
notifications,
Pagination,
@ -23,6 +22,8 @@
import { goto } from "@roxi/routify"
import OnboardingTypeModal from "./_components/OnboardingTypeModal.svelte"
import PasswordModal from "./_components/PasswordModal.svelte"
import InvitedModal from "./_components/InvitedModal.svelte"
import DeletionFailureModal from "./_components/DeletionFailureModal.svelte"
import ImportUsersModal from "./_components/ImportUsersModal.svelte"
import { createPaginationStore } from "helpers/pagination"
import { get } from "svelte/store"
@ -33,7 +34,8 @@
inviteConfirmationModal,
onboardingTypeModal,
passwordModal,
importUsersModal
importUsersModal,
deletionFailureModal
let pageInfo = createPaginationStore()
let prevEmail = undefined,
searchEmail = undefined
@ -55,6 +57,9 @@
apps: {},
}
$: userData = []
$: createUsersResponse = { successful: [], unsuccessful: [] }
$: deleteUsersResponse = { successful: [], unsuccessful: [] }
$: inviteUsersResponse = { successful: [], unsuccessful: [] }
$: page = $pageInfo.page
$: fetchUsers(page, searchEmail)
$: {
@ -92,8 +97,7 @@
admin: user.role === Constants.BudibaseRoles.Admin,
}))
try {
const res = await users.invite(payload)
notifications.success(res.message)
inviteUsersResponse = await users.invite(payload)
inviteConfirmationModal.show()
} catch (error) {
notifications.error("Error inviting user")
@ -116,8 +120,9 @@
newUsers.push(user)
}
if (!newUsers.length)
if (!newUsers.length) {
notifications.info("Duplicated! There is no new users to add.")
}
return { ...userData, users: newUsers }
}
@ -139,12 +144,14 @@
userData = await removingDuplicities({ groups, users })
if (!userData.users.length) return
return createUser()
return createUsers()
}
async function createUser() {
async function createUsers() {
try {
await users.create(await removingDuplicities(userData))
createUsersResponse = await users.create(
await removingDuplicities(userData)
)
notifications.success("Successfully created user")
await groups.actions.init()
passwordModal.show()
@ -157,7 +164,7 @@
if (onboardingType === "emailOnboarding") {
createUserFlow()
} else {
await createUser()
await createUsers()
}
}
@ -176,8 +183,15 @@
notifications.error("You cannot delete yourself")
return
}
await users.bulkDelete(ids)
notifications.success(`Successfully deleted ${selectedRows.length} rows`)
deleteUsersResponse = await users.bulkDelete(ids)
if (deleteUsersResponse.unsuccessful?.length) {
deletionFailureModal.show()
} else {
notifications.success(
`Successfully deleted ${selectedRows.length} users`
)
}
selectedRows = []
await fetchUsers(page, searchEmail)
} catch (error) {
@ -267,16 +281,7 @@
</Modal>
<Modal bind:this={inviteConfirmationModal}>
<ModalContent
showCancelButton={false}
title="Invites sent!"
confirmText="Done"
>
<Body size="S"
>Your users should now recieve an email invite to get access to their
Budibase account</Body
></ModalContent
>
<InvitedModal {inviteUsersResponse} />
</Modal>
<Modal bind:this={onboardingTypeModal}>
@ -284,7 +289,11 @@
</Modal>
<Modal bind:this={passwordModal}>
<PasswordModal userData={userData.users} />
<PasswordModal {createUsersResponse} userData={userData.users} />
</Modal>
<Modal bind:this={deletionFailureModal}>
<DeletionFailureModal {deleteUsersResponse} />
</Modal>
<Modal bind:this={importUsersModal}>

View File

@ -15,6 +15,7 @@
import { API } from "api"
import { writable } from "svelte/store"
import { redirect } from "@roxi/routify"
import { onMount } from "svelte"
// Only admins allowed here
$: {
@ -33,11 +34,12 @@
})
let loading = false
async function uploadLogo(file) {
async function uploadLogo() {
try {
let data = new FormData()
data.append("file", file)
await API.uploadLogo(data)
data.append("file", $values.logo)
await API.uploadPlugin(data)
notifications.success("Plugin uploaded successfully")
} catch (error) {
notifications.error("Error uploading logo")
}
@ -71,6 +73,11 @@
}
loading = false
}
onMount(async () => {
const plugins = await API.getPlugins()
console.log(plugins)
})
</script>
{#if $auth.isAdmin}
@ -106,6 +113,7 @@
}
}}
/>
<button on:click={uploadLogo}>Upload</button>
</div>
</div>
</div>

View File

@ -63,10 +63,14 @@ export function createUsersStore() {
return body
})
await API.createUsers({ users: mappedUsers, groups: data.groups })
const response = await API.createUsers({
users: mappedUsers,
groups: data.groups,
})
// re-search from first page
await search()
return response
}
async function del(id) {
@ -79,7 +83,7 @@ export function createUsersStore() {
}
async function bulkDelete(userIds) {
await API.deleteUsers(userIds)
return API.deleteUsers(userIds)
}
async function save(user) {

View File

@ -1,15 +1,23 @@
import { svelte } from "@sveltejs/vite-plugin-svelte"
import replace from "@rollup/plugin-replace"
import { defineConfig, loadEnv } from "vite"
import path from "path"
export default ({ mode }) => {
export default defineConfig(({ mode }) => {
const isProduction = mode === "production"
const env = loadEnv(mode, process.cwd())
return {
server: {
fs: {
strict: false,
},
hmr: {
protocol: env.VITE_HMR_PROTOCOL || "ws",
clientPort: env.VITE_HMR_CLIENT_PORT || 3000,
path: env.VITE_HMR_PATH || "/",
},
port: 3000,
},
base: "/builder/",
build: {
@ -79,4 +87,4 @@ export default ({ mode }) => {
],
},
}
}
})

View File

@ -967,6 +967,11 @@
debug "^3.1.0"
lodash.once "^4.1.1"
"@esbuild/linux-loong64@0.14.54":
version "0.14.54"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz#de2a4be678bd4d0d1ffbb86e6de779cde5999028"
integrity sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==
"@hapi/hoek@^9.0.0":
version "9.2.1"
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.2.1.tgz#9551142a1980503752536b5050fd99f4a7f13b17"
@ -1215,10 +1220,10 @@
estree-walker "^1.0.1"
picomatch "^2.2.2"
"@rollup/pluginutils@^4.1.1":
version "4.1.1"
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.1.1.tgz#1d4da86dd4eded15656a57d933fda2b9a08d47ec"
integrity sha512-clDjivHqWGXi7u+0d2r2sBi4Ie6VLEAzWMIkvJLnDmxoOhBYOTfzGbOQBA32THHm11/LiJbd01tJUpJsbshSWQ==
"@rollup/pluginutils@^4.2.1":
version "4.2.1"
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d"
integrity sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==
dependencies:
estree-walker "^2.0.1"
picomatch "^2.2.2"
@ -1350,17 +1355,17 @@
resolved "https://registry.yarnpkg.com/@spectrum-css/vars/-/vars-4.3.0.tgz#03ddf67d3aa8a9a4cb0edbbd259465c9ced7e70d"
integrity sha512-ZQ2XAhgu4G9yBeXQNDAz07Z8oZNnMt5o9vzf/mpBA7Teb/JI+8qXp2wt8D245SzmtNlFkG/bzRYvQc0scgZeCQ==
"@sveltejs/vite-plugin-svelte@1.0.0-next.19":
version "1.0.0-next.19"
resolved "https://registry.yarnpkg.com/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-1.0.0-next.19.tgz#9646abc2cd1982146db4bb341aafdb5f32f19dd2"
integrity sha512-q9hHkMzodScwDq64pNaWhekpj97vWg3wO9T0rqd8bC2EsrBQs2uD1qMJvDqlNd63AbO2uSJMGo+TQ0Xt2xgyYg==
"@sveltejs/vite-plugin-svelte@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-1.0.1.tgz#7f468f03c933fcdfc60d4773671c73f33b9ef4d6"
integrity sha512-PorCgUounn0VXcpeJu+hOweZODKmGuLHsLomwqSj+p26IwjjGffmYQfVHtiTWq+NqaUuuHWWG7vPge6UFw4Aeg==
dependencies:
"@rollup/pluginutils" "^4.1.1"
debug "^4.3.2"
kleur "^4.1.4"
magic-string "^0.25.7"
require-relative "^0.8.7"
svelte-hmr "^0.14.7"
"@rollup/pluginutils" "^4.2.1"
debug "^4.3.4"
deepmerge "^4.2.2"
kleur "^4.1.5"
magic-string "^0.26.2"
svelte-hmr "^0.14.12"
"@testing-library/dom@^7.0.3":
version "7.31.2"
@ -2493,6 +2498,13 @@ debug@^3.1.0:
dependencies:
ms "^2.1.1"
debug@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
dependencies:
ms "2.1.2"
decamelize@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
@ -2655,113 +2667,132 @@ error-ex@^1.3.1:
dependencies:
is-arrayish "^0.2.1"
esbuild-android-arm64@0.13.14:
version "0.13.14"
resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.13.14.tgz#c85083ece26be3d67e6c720e088968a98409e023"
integrity sha512-Q+Xhfp827r+ma8/DJgpMRUbDZfefsk13oePFEXEIJ4gxFbNv5+vyiYXYuKm43/+++EJXpnaYmEnu4hAKbAWYbA==
esbuild-android-64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz#505f41832884313bbaffb27704b8bcaa2d8616be"
integrity sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==
esbuild-darwin-64@0.13.14:
version "0.13.14"
resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.13.14.tgz#8e4e237ad847cc54a1d3a5caee26a746b9f0b81f"
integrity sha512-YmOhRns6QBNSjpVdTahi/yZ8dscx9ai7a6OY6z5ACgOuQuaQ2Qk2qgJ0/siZ6LgD0gJFMV8UINFV5oky5TFNQQ==
esbuild-android-arm64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz#8ce69d7caba49646e009968fe5754a21a9871771"
integrity sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==
esbuild-darwin-arm64@0.13.14:
version "0.13.14"
resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.14.tgz#b3b5ebd40b2cb06ee0f6fb342dd4bdcca54ad273"
integrity sha512-Lp00VTli2jqZghSa68fx3fEFCPsO1hK59RMo1PRap5RUjhf55OmaZTZYnCDI0FVlCtt+gBwX5qwFt4lc6tI1xg==
esbuild-darwin-64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz#24ba67b9a8cb890a3c08d9018f887cc221cdda25"
integrity sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==
esbuild-freebsd-64@0.13.14:
version "0.13.14"
resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.14.tgz#175ecb2fa8141428cf70ea2d5f4c27534bad53e0"
integrity sha512-BKosI3jtvTfnmsCW37B1TyxMUjkRWKqopR0CE9AF2ratdpkxdR24Vpe3gLKNyWiZ7BE96/SO5/YfhbPUzY8wKw==
esbuild-darwin-arm64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz#3f7cdb78888ee05e488d250a2bdaab1fa671bf73"
integrity sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==
esbuild-freebsd-arm64@0.13.14:
version "0.13.14"
resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.14.tgz#a7d64e41d1fa581f8db7775e5200f18e67d70c4d"
integrity sha512-yd2uh0yf+fWv5114+SYTl4/1oDWtr4nN5Op+PGxAkMqHfYfLjFKpcxwCo/QOS/0NWqPVE8O41IYZlFhbEN2B8Q==
esbuild-freebsd-64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz#09250f997a56ed4650f3e1979c905ffc40bbe94d"
integrity sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==
esbuild-linux-32@0.13.14:
version "0.13.14"
resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.13.14.tgz#14bdd4f6b6cfd35c65c835894651ba335c2117da"
integrity sha512-a8rOnS1oWSfkkYWXoD2yXNV4BdbDKA7PNVQ1klqkY9SoSApL7io66w5H44mTLsfyw7G6Z2vLlaLI2nz9MMAowA==
esbuild-freebsd-arm64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz#bafb46ed04fc5f97cbdb016d86947a79579f8e48"
integrity sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==
esbuild-linux-64@0.13.14:
version "0.13.14"
resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.13.14.tgz#7fd56851b2982fdd0cd8447ee9858c2c5711708a"
integrity sha512-yPZSoMs9W2MC3Dw+6kflKt5FfQm6Dicex9dGIr1OlHRsn3Hm7yGMUTctlkW53KknnZdOdcdd5upxvbxqymczVQ==
esbuild-linux-32@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz#e2a8c4a8efdc355405325033fcebeb941f781fe5"
integrity sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==
esbuild-linux-arm64@0.13.14:
version "0.13.14"
resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.14.tgz#a55634d70679ba509adeafd68eebb9fd1ec5af6c"
integrity sha512-Lvo391ln9PzC334e+jJ2S0Rt0cxP47eoH5gFyv/E8HhOnEJTvm7A+RRnMjjHnejELacTTfYgFGQYPjLsi/jObQ==
esbuild-linux-64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz#de5fdba1c95666cf72369f52b40b03be71226652"
integrity sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==
esbuild-linux-arm@0.13.14:
version "0.13.14"
resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.13.14.tgz#bb96a99677e608b31ff61f37564326d38e846ca2"
integrity sha512-8chZE4pkKRvJ/M/iwsNQ1KqsRg2RyU5eT/x2flNt/f8F2TVrDreR7I0HEeCR50wLla3B1C3wTIOzQBmjuc6uWg==
esbuild-linux-arm64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz#dae4cd42ae9787468b6a5c158da4c84e83b0ce8b"
integrity sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==
esbuild-linux-mips64le@0.13.14:
version "0.13.14"
resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.14.tgz#6a55362a8fd1e593dea2ecc41877beed8b8184b9"
integrity sha512-MZhgxbmrWbpY3TOE029O6l5tokG9+Yoj2hW7vdit/d/VnmneqeGrSHADuDL6qXM8L5jaCiaivb4VhsyVCpdAbQ==
esbuild-linux-arm@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz#a2c1dff6d0f21dbe8fc6998a122675533ddfcd59"
integrity sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==
esbuild-linux-ppc64le@0.13.14:
version "0.13.14"
resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.14.tgz#9e0048587ece0a7f184ab147f20d077098045e7f"
integrity sha512-un7KMwS7fX1Un6BjfSZxTT8L5cV/8Uf4SAhM7WYy2XF8o8TI+uRxxD03svZnRNIPsN2J5cl6qV4n7Iwz+yhhVw==
esbuild-linux-mips64le@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz#d9918e9e4cb972f8d6dae8e8655bf9ee131eda34"
integrity sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==
esbuild-netbsd-64@0.13.14:
version "0.13.14"
resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.14.tgz#dcab16a4bbcfa16e2e8535dadc5f64fdc891c63b"
integrity sha512-5ekKx/YbOmmlTeNxBjh38Uh5TGn5C4uyqN17i67k18pS3J+U2hTVD7rCxcFcRS1AjNWumkVL3jWqYXadFwMS0Q==
esbuild-linux-ppc64le@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz#3f9a0f6d41073fb1a640680845c7de52995f137e"
integrity sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==
esbuild-openbsd-64@0.13.14:
version "0.13.14"
resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.14.tgz#3c7453b155ebb68dc34d5aec3bd6505337bdda08"
integrity sha512-9bzvwewHjct2Cv5XcVoE1yW5YTW12Sk838EYfA46abgnhxGoFSD1mFcaztp5HHC43AsF+hQxbSFG/RilONARUA==
esbuild-linux-riscv64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz#618853c028178a61837bc799d2013d4695e451c8"
integrity sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==
esbuild-sunos-64@0.13.14:
version "0.13.14"
resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.13.14.tgz#85addf5fef6b5db154a955d4f2e88953359d75ce"
integrity sha512-mjMrZB76M6FmoiTvj/RGWilrioR7gVwtFBRVugr9qLarXMIU1W/pQx+ieEOtflrW61xo8w1fcxyHsVVGRvoQ0w==
esbuild-linux-s390x@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz#d1885c4c5a76bbb5a0fe182e2c8c60eb9e29f2a6"
integrity sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==
esbuild-windows-32@0.13.14:
version "0.13.14"
resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.13.14.tgz#f77f98f30a5c636c44db2428ecdf9bcbbaedb1a7"
integrity sha512-GZa6mrx2rgfbH/5uHg0Rdw50TuOKbdoKCpEBitzmG5tsXBdce+cOL+iFO5joZc6fDVCLW3Y6tjxmSXRk/v20Hg==
esbuild-netbsd-64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz#69ae917a2ff241b7df1dbf22baf04bd330349e81"
integrity sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==
esbuild-windows-64@0.13.14:
version "0.13.14"
resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.13.14.tgz#bc778674c40d65150d12385e0f23eb3a0badbd0d"
integrity sha512-Lsgqah24bT7ClHjLp/Pj3A9wxjhIAJyWQcrOV4jqXAFikmrp2CspA8IkJgw7HFjx6QrJuhpcKVbCAe/xw0i2yw==
esbuild-openbsd-64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz#db4c8495287a350a6790de22edea247a57c5d47b"
integrity sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==
esbuild-windows-arm64@0.13.14:
version "0.13.14"
resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.14.tgz#91a8dad35ab2c4dd27cd83860742955b25a354d7"
integrity sha512-KP8FHVlWGhM7nzYtURsGnskXb/cBCPTfj0gOKfjKq2tHtYnhDZywsUG57nk7TKhhK0fL11LcejHG3LRW9RF/9A==
esbuild-sunos-64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz#54287ee3da73d3844b721c21bc80c1dc7e1bf7da"
integrity sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==
esbuild@^0.13.2:
version "0.13.14"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.13.14.tgz#98a3f7f42809abdc2b57c84565d0f713382dc1a5"
integrity sha512-xu4D+1ji9x53ocuomcY+KOrwAnWzhBu/wTEjpdgZ8I1c8i5vboYIeigMdzgY1UowYBKa2vZgVgUB32bu7gkxeg==
esbuild-windows-32@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz#f8aaf9a5667630b40f0fb3aa37bf01bbd340ce31"
integrity sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==
esbuild-windows-64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz#bf54b51bd3e9b0f1886ffdb224a4176031ea0af4"
integrity sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==
esbuild-windows-arm64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz#937d15675a15e4b0e4fafdbaa3a01a776a2be982"
integrity sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==
esbuild@^0.14.47:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.54.tgz#8b44dcf2b0f1a66fc22459943dccf477535e9aa2"
integrity sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==
optionalDependencies:
esbuild-android-arm64 "0.13.14"
esbuild-darwin-64 "0.13.14"
esbuild-darwin-arm64 "0.13.14"
esbuild-freebsd-64 "0.13.14"
esbuild-freebsd-arm64 "0.13.14"
esbuild-linux-32 "0.13.14"
esbuild-linux-64 "0.13.14"
esbuild-linux-arm "0.13.14"
esbuild-linux-arm64 "0.13.14"
esbuild-linux-mips64le "0.13.14"
esbuild-linux-ppc64le "0.13.14"
esbuild-netbsd-64 "0.13.14"
esbuild-openbsd-64 "0.13.14"
esbuild-sunos-64 "0.13.14"
esbuild-windows-32 "0.13.14"
esbuild-windows-64 "0.13.14"
esbuild-windows-arm64 "0.13.14"
"@esbuild/linux-loong64" "0.14.54"
esbuild-android-64 "0.14.54"
esbuild-android-arm64 "0.14.54"
esbuild-darwin-64 "0.14.54"
esbuild-darwin-arm64 "0.14.54"
esbuild-freebsd-64 "0.14.54"
esbuild-freebsd-arm64 "0.14.54"
esbuild-linux-32 "0.14.54"
esbuild-linux-64 "0.14.54"
esbuild-linux-arm "0.14.54"
esbuild-linux-arm64 "0.14.54"
esbuild-linux-mips64le "0.14.54"
esbuild-linux-ppc64le "0.14.54"
esbuild-linux-riscv64 "0.14.54"
esbuild-linux-s390x "0.14.54"
esbuild-netbsd-64 "0.14.54"
esbuild-openbsd-64 "0.14.54"
esbuild-sunos-64 "0.14.54"
esbuild-windows-32 "0.14.54"
esbuild-windows-64 "0.14.54"
esbuild-windows-arm64 "0.14.54"
escalade@^3.1.1:
version "3.1.1"
@ -3519,6 +3550,13 @@ is-core-module@^2.2.0:
dependencies:
has "^1.0.3"
is-core-module@^2.9.0:
version "2.10.0"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.10.0.tgz#9012ede0a91c69587e647514e1d5277019e728ed"
integrity sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==
dependencies:
has "^1.0.3"
is-data-descriptor@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
@ -4285,10 +4323,10 @@ kleur@^3.0.3:
resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
kleur@^4.1.4:
version "4.1.4"
resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.4.tgz#8c202987d7e577766d039a8cd461934c01cda04d"
integrity sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA==
kleur@^4.1.5:
version "4.1.5"
resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780"
integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==
lazy-ass@1.6.0, lazy-ass@^1.6.0:
version "1.6.0"
@ -4425,6 +4463,13 @@ magic-string@^0.25.7:
dependencies:
sourcemap-codec "^1.4.4"
magic-string@^0.26.2:
version "0.26.2"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.26.2.tgz#5331700e4158cd6befda738bb6b0c7b93c0d4432"
integrity sha512-NzzlXpclt5zAbmo6h6jNc8zl2gNRGHvmsZW4IvZhTC4W7k4OlLP+S5YLussa/r3ixNT66KOQfNORlXHSOy/X4A==
dependencies:
sourcemap-codec "^1.4.8"
make-dir@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
@ -4615,10 +4660,10 @@ nanoid@^2.1.0:
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.1.11.tgz#ec24b8a758d591561531b4176a01e3ab4f0f0280"
integrity sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==
nanoid@^3.1.30:
version "3.1.30"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.30.tgz#63f93cc548d2a113dc5dfbc63bfa09e2b9b64362"
integrity sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==
nanoid@^3.3.4:
version "3.3.4"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
nanomatch@^1.2.9:
version "1.2.13"
@ -4887,7 +4932,7 @@ path-key@^3.0.0, path-key@^3.1.0:
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
path-parse@^1.0.6:
path-parse@^1.0.6, path-parse@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
@ -4948,14 +4993,14 @@ posix-character-classes@^0.1.0:
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=
postcss@^8.3.8:
version "8.3.11"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.11.tgz#c3beca7ea811cd5e1c4a3ec6d2e7599ef1f8f858"
integrity sha512-hCmlUAIlUiav8Xdqw3Io4LcpA1DOt7h3LSTAC4G6JGHFFaWzI6qvFt9oilvl8BmkbBRX1IhM90ZAmpk68zccQA==
postcss@^8.4.16:
version "8.4.16"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.16.tgz#33a1d675fac39941f5f445db0de4db2b6e01d43c"
integrity sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==
dependencies:
nanoid "^3.1.30"
nanoid "^3.3.4"
picocolors "^1.0.0"
source-map-js "^0.6.2"
source-map-js "^1.0.2"
posthog-js@1.4.5:
version "1.4.5"
@ -5184,11 +5229,6 @@ require-main-filename@^2.0.0:
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
require-relative@^0.8.7:
version "0.8.7"
resolved "https://registry.yarnpkg.com/require-relative/-/require-relative-0.8.7.tgz#7999539fc9e047a37928fa196f8e1563dabd36de"
integrity sha1-eZlTn8ngR6N5KPoZb44VY9q9Nt4=
resolve-cwd@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"
@ -5206,7 +5246,7 @@ resolve-url@^0.2.1:
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
resolve@^1.10.0, resolve@^1.14.2, resolve@^1.18.1, resolve@^1.20.0:
resolve@^1.10.0, resolve@^1.14.2, resolve@^1.18.1:
version "1.20.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
@ -5214,6 +5254,15 @@ resolve@^1.10.0, resolve@^1.14.2, resolve@^1.18.1, resolve@^1.20.0:
is-core-module "^2.2.0"
path-parse "^1.0.6"
resolve@^1.22.1:
version "1.22.1"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==
dependencies:
is-core-module "^2.9.0"
path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0"
restore-cursor@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e"
@ -5262,7 +5311,14 @@ rollup-pluginutils@^2.8.2:
dependencies:
estree-walker "^0.6.1"
rollup@^2.44.0, rollup@^2.57.0:
"rollup@>=2.75.6 <2.77.0 || ~2.77.0":
version "2.77.3"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.77.3.tgz#8f00418d3a2740036e15deb653bed1a90ee0cc12"
integrity sha512-/qxNTG7FbmefJWoeeYJFbHehJ2HNWnjkAFRKzWN/45eNBBF/r8lo992CwcJXEzyVxs5FmfId+vTSTQDb+bxA+g==
optionalDependencies:
fsevents "~2.3.2"
rollup@^2.44.0:
version "2.60.0"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.60.0.tgz#4ee60ab7bdd0356763f87d7099f413e5460fc193"
integrity sha512-cHdv9GWd58v58rdseC8e8XIaPUo8a9cgZpnCMMDGZFDZKEODOiPPEQFXLriWr/TjXzhPPmG5bkAztPsOARIcGQ==
@ -5475,10 +5531,10 @@ snapdragon@^0.8.1:
source-map-resolve "^0.5.0"
use "^3.1.0"
source-map-js@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e"
integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==
source-map-js@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
source-map-resolve@^0.5.0:
version "0.5.3"
@ -5527,7 +5583,7 @@ source-map@^0.7.3:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
sourcemap-codec@^1.4.4:
sourcemap-codec@^1.4.4, sourcemap-codec@^1.4.8:
version "1.4.8"
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
@ -5707,15 +5763,20 @@ supports-hyperlinks@^2.0.0:
has-flag "^4.0.0"
supports-color "^7.0.0"
supports-preserve-symlinks-flag@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
svelte-dnd-action@^0.9.8:
version "0.9.12"
resolved "https://registry.yarnpkg.com/svelte-dnd-action/-/svelte-dnd-action-0.9.12.tgz#78cf33097986488c6d069eca517af473cd998730"
integrity sha512-GlXIB3/56IMR5A0+qUx+FX7Q7n8uCAIeuYdgSBmn9iOlxWc+mgM8P1kNwAKCMSTdQ4IQETVQILNgWVY1KIFzsg==
svelte-hmr@^0.14.7:
version "0.14.7"
resolved "https://registry.yarnpkg.com/svelte-hmr/-/svelte-hmr-0.14.7.tgz#7fa8261c7b225d9409f0a86f3b9ea5c3ca6f6607"
integrity sha512-pDrzgcWSoMaK6AJkBWkmgIsecW0GChxYZSZieIYfCP0v2oPyx2CYU/zm7TBIcjLVUPP714WxmViE9Thht4etog==
svelte-hmr@^0.14.12:
version "0.14.12"
resolved "https://registry.yarnpkg.com/svelte-hmr/-/svelte-hmr-0.14.12.tgz#a127aec02f1896500b10148b2d4d21ddde39973f"
integrity sha512-4QSW/VvXuqVcFZ+RhxiR8/newmwOCTlbYIezvkeN6302YFRE8cXy0naamHcjz8Y9Ce3ITTZtrHrIL0AGfyo61w==
svelte-jester@^1.3.2:
version "1.8.2"
@ -6081,15 +6142,15 @@ verror@1.10.0:
core-util-is "1.0.2"
extsprintf "^1.2.0"
vite@^2.1.5:
version "2.6.14"
resolved "https://registry.yarnpkg.com/vite/-/vite-2.6.14.tgz#35c09a15e4df823410819a2a239ab11efb186271"
integrity sha512-2HA9xGyi+EhY2MXo0+A2dRsqsAG3eFNEVIo12olkWhOmc8LfiM+eMdrXf+Ruje9gdXgvSqjLI9freec1RUM5EA==
vite@^3.0.8:
version "3.0.8"
resolved "https://registry.yarnpkg.com/vite/-/vite-3.0.8.tgz#aa095ad8e3e5da46d9ec7e878f262678965d6531"
integrity sha512-AOZ4eN7mrkJiOLuw8IA7piS4IdOQyQCA81GxGsAQvAZzMRi9ZwGB3TOaYsj4uLAWK46T5L4AfQ6InNGlxX30IQ==
dependencies:
esbuild "^0.13.2"
postcss "^8.3.8"
resolve "^1.20.0"
rollup "^2.57.0"
esbuild "^0.14.47"
postcss "^8.4.16"
resolve "^1.22.1"
rollup ">=2.75.6 <2.77.0 || ~2.77.0"
optionalDependencies:
fsevents "~2.3.2"

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/cli",
"version": "1.2.44-alpha.2",
"version": "1.3.12-alpha.3",
"description": "Budibase CLI, for developers, self hosting and migrations.",
"main": "src/index.js",
"bin": {
@ -26,14 +26,18 @@
"outputPath": "build"
},
"dependencies": {
"@budibase/backend-core": "1.2.41-alpha.5",
"@budibase/backend-core": "1.3.12-alpha.3",
"@budibase/string-templates": "1.3.12-alpha.3",
"@budibase/types": "1.3.12-alpha.3",
"axios": "0.21.2",
"chalk": "4.1.0",
"cli-progress": "3.11.2",
"commander": "7.1.0",
"docker-compose": "0.23.6",
"dotenv": "16.0.1",
"download": "^8.0.0",
"inquirer": "8.0.0",
"joi": "^17.6.0",
"lookpath": "1.1.0",
"node-fetch": "2",
"pkg": "5.7.0",

View File

@ -3,6 +3,7 @@ exports.CommandWords = {
HOSTING: "hosting",
ANALYTICS: "analytics",
HELP: "help",
PLUGIN: "plugins",
}
exports.InitTypes = {

View File

@ -0,0 +1 @@
process.env.NO_JS = "1"

27
packages/cli/src/exec.js Normal file
View File

@ -0,0 +1,27 @@
const util = require("util")
const exec = util.promisify(require("child_process").exec)
exports.exec = async (command, dir = "./") => {
const { stdout } = await exec(command, { cwd: dir })
return stdout
}
exports.utilityInstalled = async utilName => {
try {
await exports.exec(`${utilName} --version`)
return true
} catch (err) {
return false
}
}
exports.runPkgCommand = async (command, dir = "./") => {
const yarn = await exports.utilityInstalled("yarn")
const npm = await exports.utilityInstalled("npm")
if (!yarn && !npm) {
throw new Error("Must have yarn or npm installed to run build.")
}
const npmCmd = command === "install" ? `npm ${command}` : `npm run ${command}`
const cmd = yarn ? `yarn ${command}` : npmCmd
await exports.exec(cmd, dir)
}

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