Merge branch 'develop' of github.com:Budibase/budibase into public-api-sdk

This commit is contained in:
Andrew Kingston 2022-09-16 11:54:18 +01:00
commit 338287c602
213 changed files with 6576 additions and 1267 deletions

View File

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

1
.gitignore vendored
View File

@ -63,6 +63,7 @@ typings/
# dotenv environment variables file # dotenv environment variables file
.env .env
!qa-core/.env
!hosting/.env !hosting/.env
hosting/.generated-nginx.dev.conf hosting/.generated-nginx.dev.conf
hosting/proxy/.generated-nginx.prod.conf hosting/proxy/.generated-nginx.prod.conf

View File

@ -9,7 +9,11 @@ if [[ "${TARGETBUILD}" = "aas" ]]; then
chown -R couchdb:couchdb $DATA_DIR/couch/ chown -R couchdb:couchdb $DATA_DIR/couch/
apt update apt update
apt-get install -y openssh-server apt-get install -y openssh-server
sed -i "s/#Port 22/Port 2222/" /etc/ssh/sshd_config echo "root:Docker!" | chpasswd
mkdir -p /tmp
chmod +x /tmp/ssh_setup.sh \
&& (sleep 1;/tmp/ssh_setup.sh 2>&1 > /dev/null)
cp /etc/sshd_config /etc/ssh/sshd_config
/etc/init.d/ssh restart /etc/init.d/ssh restart
sed -i "s#DATA_DIR#/home#g" /opt/clouseau/clouseau.ini sed -i "s#DATA_DIR#/home#g" /opt/clouseau/clouseau.ini
sed -i "s#DATA_DIR#/home#g" /opt/couchdb/etc/local.ini sed -i "s#DATA_DIR#/home#g" /opt/couchdb/etc/local.ini

View File

@ -29,23 +29,8 @@ ENV TARGETBUILD $TARGETBUILD
COPY --from=build /app /app COPY --from=build /app /app
COPY --from=build /worker /worker COPY --from=build /worker /worker
ENV \ # ENV CUSTOM_DOMAIN=budi001.custom.com \
APP_PORT=4001 \ # See runner.sh for Env Vars
ARCHITECTURE=amd \
BUDIBASE_ENVIRONMENT=PRODUCTION \
CLUSTER_PORT=80 \
# CUSTOM_DOMAIN=budi001.custom.com \
DATA_DIR=/data \
DEPLOYMENT_ENVIRONMENT=docker \
MINIO_URL=http://localhost:9000 \
POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU \
REDIS_URL=localhost:6379 \
SELF_HOSTED=1 \
TARGETBUILD=$TARGETBUILD \
WORKER_PORT=4002 \
WORKER_URL=http://localhost:4002 \
APPS_URL=http://localhost:4001
# These secret env variables are generated by the runner at startup # These secret env variables are generated by the runner at startup
# their values can be overriden by the user, they will be written # their values can be overriden by the user, they will be written
# to the .env file in the /data directory for use later on # to the .env file in the /data directory for use later on
@ -117,6 +102,8 @@ RUN chmod +x ./build-target-paths.sh
# Script below sets the path for storing data based on $DATA_DIR # Script below sets the path for storing data based on $DATA_DIR
# For Azure App Service install SSH & point data locations to /home # For Azure App Service install SSH & point data locations to /home
ADD hosting/single/ssh/sshd_config /etc/
ADD hosting/single/ssh/ssh_setup.sh /tmp
RUN /build-target-paths.sh RUN /build-target-paths.sh
# cleanup cache # cleanup cache
@ -124,6 +111,8 @@ RUN yarn cache clean -f
EXPOSE 80 EXPOSE 80
EXPOSE 443 EXPOSE 443
# Expose port 2222 for SSH on Azure App Service build
EXPOSE 2222
VOLUME /data VOLUME /data
# setup letsencrypt certificate # setup letsencrypt certificate

View File

@ -1,6 +1,21 @@
#!/bin/bash #!/bin/bash
declare -a ENV_VARS=("COUCHDB_USER" "COUCHDB_PASSWORD" "DATA_DIR" "MINIO_ACCESS_KEY" "MINIO_SECRET_KEY" "INTERNAL_API_KEY" "JWT_SECRET" "REDIS_PASSWORD") declare -a ENV_VARS=("COUCHDB_USER" "COUCHDB_PASSWORD" "DATA_DIR" "MINIO_ACCESS_KEY" "MINIO_SECRET_KEY" "INTERNAL_API_KEY" "JWT_SECRET" "REDIS_PASSWORD")
declare -a DOCKER_VARS=("APP_PORT" "APPS_URL" "ARCHITECTURE" "BUDIBASE_ENVIRONMENT" "CLUSTER_PORT" "DEPLOYMENT_ENVIRONMENT" "MINIO_URL" "NODE_ENV" "POSTHOG_TOKEN" "REDIS_URL" "SELF_HOSTED" "WORKER_PORT" "WORKER_URL")
# Check the env vars set in Dockerfile have come through, AAS seems to drop them
[[ -z "${APP_PORT}" ]] && export APP_PORT=4001
[[ -z "${ARCHITECTURE}" ]] && export ARCHITECTURE=amd
[[ -z "${BUDIBASE_ENVIRONMENT}" ]] && export BUDIBASE_ENVIRONMENT=PRODUCTION
[[ -z "${CLUSTER_PORT}" ]] && export CLUSTER_PORT=80
[[ -z "${DEPLOYMENT_ENVIRONMENT}" ]] && export DEPLOYMENT_ENVIRONMENT=docker
[[ -z "${MINIO_URL}" ]] && export MINIO_URL=http://localhost:9000
[[ -z "${NODE_ENV}" ]] && export NODE_ENV=production
[[ -z "${POSTHOG_TOKEN}" ]] && export POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU
[[ -z "${REDIS_URL}" ]] && export REDIS_URL=localhost:6379
[[ -z "${SELF_HOSTED}" ]] && export SELF_HOSTED=1
[[ -z "${WORKER_PORT}" ]] && export WORKER_PORT=4002
[[ -z "${WORKER_URL}" ]] && export WORKER_URL=http://localhost:4002
[[ -z "${APPS_URL}" ]] && export APPS_URL=http://localhost:4001
# export CUSTOM_DOMAIN=budi001.custom.com
# Azure App Service customisations # Azure App Service customisations
if [[ "${TARGETBUILD}" = "aas" ]]; then if [[ "${TARGETBUILD}" = "aas" ]]; then
DATA_DIR=/home DATA_DIR=/home
@ -10,9 +25,10 @@ else
fi fi
if [ -f "${DATA_DIR}/.env" ]; then if [ -f "${DATA_DIR}/.env" ]; then
export $(cat ${DATA_DIR}/.env | xargs) # Read in the .env file and export the variables
for LINE in $(cat ${DATA_DIR}/.env); do export $LINE; done
fi fi
# first randomise any unset environment variables # randomise any unset environment variables
for ENV_VAR in "${ENV_VARS[@]}" for ENV_VAR in "${ENV_VARS[@]}"
do do
temp=$(eval "echo \$$ENV_VAR") temp=$(eval "echo \$$ENV_VAR")
@ -30,11 +46,18 @@ if [ ! -f "${DATA_DIR}/.env" ]; then
temp=$(eval "echo \$$ENV_VAR") temp=$(eval "echo \$$ENV_VAR")
echo "$ENV_VAR=$temp" >> ${DATA_DIR}/.env echo "$ENV_VAR=$temp" >> ${DATA_DIR}/.env
done done
for ENV_VAR in "${DOCKER_VARS[@]}"
do
temp=$(eval "echo \$$ENV_VAR")
echo "$ENV_VAR=$temp" >> ${DATA_DIR}/.env
done
echo "COUCH_DB_URL=${COUCH_DB_URL}" >> ${DATA_DIR}/.env echo "COUCH_DB_URL=${COUCH_DB_URL}" >> ${DATA_DIR}/.env
fi fi
export COUCH_DB_URL=http://$COUCHDB_USER:$COUCHDB_PASSWORD@localhost:5984 # Read in the .env file and export the variables
for LINE in $(cat ${DATA_DIR}/.env); do export $LINE; done
ln -s ${DATA_DIR}/.env /app/.env
ln -s ${DATA_DIR}/.env /worker/.env
# make these directories in runner, incase of mount # make these directories in runner, incase of mount
mkdir -p ${DATA_DIR}/couch/{dbs,views} mkdir -p ${DATA_DIR}/couch/{dbs,views}
mkdir -p ${DATA_DIR}/minio mkdir -p ${DATA_DIR}/minio

View File

@ -0,0 +1,8 @@
#!/bin/sh
ssh-keygen -A
#prepare run dir
if [ ! -d "/var/run/sshd" ]; then
mkdir -p /var/run/sshd
fi

View File

@ -0,0 +1,12 @@
Port 2222
ListenAddress 0.0.0.0
LoginGraceTime 180
X11Forwarding yes
Ciphers aes128-cbc,3des-cbc,aes256-cbc,aes128-ctr,aes192-ctr,aes256-ctr
MACs hmac-sha1,hmac-sha1-96
StrictModes yes
SyslogFacility DAEMON
PasswordAuthentication yes
PermitEmptyPasswords no
PermitRootLogin yes
Subsystem sftp internal-sftp

View File

@ -1,5 +1,5 @@
{ {
"version": "1.3.15-alpha.7", "version": "1.3.19-alpha.6",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -45,8 +45,8 @@
"lint:eslint": "eslint packages", "lint:eslint": "eslint packages",
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\"", "lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\"",
"lint": "yarn run lint:eslint && yarn run lint:prettier", "lint": "yarn run lint:eslint && yarn run lint:prettier",
"lint:fix:eslint": "eslint --fix packages", "lint:fix:eslint": "eslint --fix packages qa-core",
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\"", "lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --write \"qa-core/**/*.{js,ts,svelte}\"",
"lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint", "lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint",
"test:e2e": "lerna run cy:test --stream", "test:e2e": "lerna run cy:test --stream",
"test:e2e:ci": "lerna run cy:ci --stream", "test:e2e:ci": "lerna run cy:ci --stream",

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/backend-core", "name": "@budibase/backend-core",
"version": "1.3.15-alpha.7", "version": "1.3.19-alpha.6",
"description": "Budibase backend core libraries used in server and worker", "description": "Budibase backend core libraries used in server and worker",
"main": "dist/src/index.js", "main": "dist/src/index.js",
"types": "dist/src/index.d.ts", "types": "dist/src/index.d.ts",
@ -20,7 +20,7 @@
"test:watch": "jest --watchAll" "test:watch": "jest --watchAll"
}, },
"dependencies": { "dependencies": {
"@budibase/types": "1.3.15-alpha.7", "@budibase/types": "1.3.19-alpha.6",
"@shopify/jest-koa-mocks": "5.0.1", "@shopify/jest-koa-mocks": "5.0.1",
"@techpass/passport-openidconnect": "0.3.2", "@techpass/passport-openidconnect": "0.3.2",
"aws-sdk": "2.1030.0", "aws-sdk": "2.1030.0",

View File

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

View File

@ -19,6 +19,7 @@ export enum ViewName {
ROUTING = "screen_routes", ROUTING = "screen_routes",
AUTOMATION_LOGS = "automation_logs", AUTOMATION_LOGS = "automation_logs",
ACCOUNT_BY_EMAIL = "account_by_email", ACCOUNT_BY_EMAIL = "account_by_email",
PLATFORM_USERS_LOWERCASE = "platform_users_lowercase",
} }
export const DeprecatedViews = { export const DeprecatedViews = {

View File

@ -2,7 +2,8 @@ import { newid } from "../hashing"
import { DEFAULT_TENANT_ID, Configs } from "../constants" import { DEFAULT_TENANT_ID, Configs } from "../constants"
import env from "../environment" import env from "../environment"
import { SEPARATOR, DocumentType, UNICODE_MAX, ViewName } from "./constants" import { SEPARATOR, DocumentType, UNICODE_MAX, ViewName } from "./constants"
import { getTenantId, getGlobalDBName, getGlobalDB } from "../tenancy" import { getTenantId, getGlobalDB } from "../context"
import { getGlobalDBName } from "../tenancy/utils"
import fetch from "node-fetch" import fetch from "node-fetch"
import { doWithDB, allDbs } from "./index" import { doWithDB, allDbs } from "./index"
import { getCouchInfo } from "./pouch" import { getCouchInfo } from "./pouch"

View File

@ -1,203 +0,0 @@
const {
DocumentType,
ViewName,
DeprecatedViews,
SEPARATOR,
} = require("./utils")
const { getGlobalDB } = require("../tenancy")
const { StaticDatabases } = require("./constants")
const { doWithDB } = require("./")
const DESIGN_DB = "_design/database"
function DesignDoc() {
return {
_id: DESIGN_DB,
// view collation information, read before writing any complex views:
// https://docs.couchdb.org/en/master/ddocs/views/collation.html#collation-specification
views: {},
}
}
async function removeDeprecated(db, viewName) {
if (!DeprecatedViews[viewName]) {
return
}
try {
const designDoc = await db.get(DESIGN_DB)
for (let deprecatedNames of DeprecatedViews[viewName]) {
delete designDoc.views[deprecatedNames]
}
await db.put(designDoc)
} catch (err) {
// doesn't exist, ignore
}
}
exports.createNewUserEmailView = async () => {
const db = getGlobalDB()
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.USER}${SEPARATOR}")) {
emit(doc.email.toLowerCase(), doc._id)
}
}`,
}
designDoc.views = {
...designDoc.views,
[ViewName.USER_BY_EMAIL]: view,
}
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
try {
designDoc = await db.get("_design/database")
} catch (err) {
// no design doc, make one
designDoc = DesignDoc()
}
const view = {
// if using variables in a map function need to inject them before use
map: `function(doc) {
if (doc._id.startsWith("${DocumentType.USER}${SEPARATOR}") && doc.roles) {
for (let prodAppId of Object.keys(doc.roles)) {
let emitted = prodAppId + "${SEPARATOR}" + doc._id
emit(emitted, null)
}
}
}`,
}
designDoc.views = {
...designDoc.views,
[ViewName.USER_BY_APP]: view,
}
await db.put(designDoc)
}
exports.createApiKeyView = async () => {
const db = getGlobalDB()
let designDoc
try {
designDoc = await db.get("_design/database")
} catch (err) {
designDoc = DesignDoc()
}
const view = {
map: `function(doc) {
if (doc._id.startsWith("${DocumentType.DEV_INFO}") && doc.apiKey) {
emit(doc.apiKey, doc.userId)
}
}`,
}
designDoc.views = {
...designDoc.views,
[ViewName.BY_API_KEY]: view,
}
await db.put(designDoc)
}
exports.createUserBuildersView = async () => {
const db = getGlobalDB()
let designDoc
try {
designDoc = await db.get("_design/database")
} catch (err) {
// no design doc, make one
designDoc = DesignDoc()
}
const view = {
map: `function(doc) {
if (doc.builder && doc.builder.global === true) {
emit(doc._id, doc._id)
}
}`,
}
designDoc.views = {
...designDoc.views,
[ViewName.USER_BY_BUILDERS]: view,
}
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,
[ViewName.BY_API_KEY]: exports.createApiKeyView,
[ViewName.USER_BY_BUILDERS]: exports.createUserBuildersView,
[ViewName.USER_BY_APP]: exports.createUserAppView,
}
// can pass DB in if working with something specific
if (!db) {
db = getGlobalDB()
}
return exports.queryView(viewName, params, db, CreateFuncByName)
}

View File

@ -0,0 +1,261 @@
import { DocumentType, ViewName, DeprecatedViews, SEPARATOR } from "./utils"
import { getGlobalDB } from "../context"
import PouchDB from "pouchdb"
import { StaticDatabases } from "./constants"
import { doWithDB } from "./"
const DESIGN_DB = "_design/database"
function DesignDoc() {
return {
_id: DESIGN_DB,
// view collation information, read before writing any complex views:
// https://docs.couchdb.org/en/master/ddocs/views/collation.html#collation-specification
views: {},
}
}
interface DesignDocument {
views: any
}
async function removeDeprecated(db: PouchDB.Database, viewName: ViewName) {
// @ts-ignore
if (!DeprecatedViews[viewName]) {
return
}
try {
const designDoc = await db.get<DesignDocument>(DESIGN_DB)
// @ts-ignore
for (let deprecatedNames of DeprecatedViews[viewName]) {
delete designDoc.views[deprecatedNames]
}
await db.put(designDoc)
} catch (err) {
// doesn't exist, ignore
}
}
export const createNewUserEmailView = async () => {
const db = getGlobalDB()
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.USER}${SEPARATOR}")) {
emit(doc.email.toLowerCase(), doc._id)
}
}`,
}
designDoc.views = {
...designDoc.views,
[ViewName.USER_BY_EMAIL]: view,
}
await db.put(designDoc)
}
export const createAccountEmailView = async () => {
await doWithDB(
StaticDatabases.PLATFORM_INFO.name,
async (db: PouchDB.Database) => {
let designDoc
try {
designDoc = await db.get<DesignDocument>(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)
}
)
}
export const createUserAppView = async () => {
const db = getGlobalDB() as PouchDB.Database
let designDoc
try {
designDoc = await db.get<DesignDocument>("_design/database")
} catch (err) {
// no design doc, make one
designDoc = DesignDoc()
}
const view = {
// if using variables in a map function need to inject them before use
map: `function(doc) {
if (doc._id.startsWith("${DocumentType.USER}${SEPARATOR}") && doc.roles) {
for (let prodAppId of Object.keys(doc.roles)) {
let emitted = prodAppId + "${SEPARATOR}" + doc._id
emit(emitted, null)
}
}
}`,
}
designDoc.views = {
...designDoc.views,
[ViewName.USER_BY_APP]: view,
}
await db.put(designDoc)
}
export const createApiKeyView = async () => {
const db = getGlobalDB()
let designDoc
try {
designDoc = await db.get("_design/database")
} catch (err) {
designDoc = DesignDoc()
}
const view = {
map: `function(doc) {
if (doc._id.startsWith("${DocumentType.DEV_INFO}") && doc.apiKey) {
emit(doc.apiKey, doc.userId)
}
}`,
}
designDoc.views = {
...designDoc.views,
[ViewName.BY_API_KEY]: view,
}
await db.put(designDoc)
}
export const createUserBuildersView = async () => {
const db = getGlobalDB()
let designDoc
try {
designDoc = await db.get("_design/database")
} catch (err) {
// no design doc, make one
designDoc = DesignDoc()
}
const view = {
map: `function(doc) {
if (doc.builder && doc.builder.global === true) {
emit(doc._id, doc._id)
}
}`,
}
designDoc.views = {
...designDoc.views,
[ViewName.USER_BY_BUILDERS]: view,
}
await db.put(designDoc)
}
export const createPlatformUserView = async () => {
await doWithDB(
StaticDatabases.PLATFORM_INFO.name,
async (db: PouchDB.Database) => {
let designDoc
try {
designDoc = await db.get<DesignDocument>(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.tenantId) {
emit(doc._id.toLowerCase(), doc._id)
}
}`,
}
designDoc.views = {
...designDoc.views,
[ViewName.PLATFORM_USERS_LOWERCASE]: view,
}
await db.put(designDoc)
}
)
}
export interface QueryViewOptions {
arrayResponse?: boolean
}
export const queryView = async <T>(
viewName: ViewName,
params: PouchDB.Query.Options<T, T>,
db: PouchDB.Database,
CreateFuncByName: any,
opts?: QueryViewOptions
): Promise<T[] | T | undefined> => {
try {
let response = await db.query<T, T>(`database/${viewName}`, params)
const rows = response.rows
const docs = rows.map(row => (params.include_docs ? row.doc : row.value))
// if arrayResponse has been requested, always return array regardless of length
if (opts?.arrayResponse) {
return docs
} else {
// return the single document if there is only one
return docs.length <= 1 ? docs[0] : docs
}
} catch (err: any) {
if (err != null && err.name === "not_found") {
const createFunc = CreateFuncByName[viewName]
await removeDeprecated(db, viewName)
await createFunc()
return queryView(viewName, params, db, CreateFuncByName, opts)
} else {
throw err
}
}
}
export const queryPlatformView = async <T>(
viewName: ViewName,
params: PouchDB.Query.Options<T, T>,
opts?: QueryViewOptions
): Promise<T[] | T | undefined> => {
const CreateFuncByName = {
[ViewName.ACCOUNT_BY_EMAIL]: createAccountEmailView,
[ViewName.PLATFORM_USERS_LOWERCASE]: createPlatformUserView,
}
return doWithDB(
StaticDatabases.PLATFORM_INFO.name,
async (db: PouchDB.Database) => {
return queryView(viewName, params, db, CreateFuncByName, opts)
}
)
}
export const queryGlobalView = async <T>(
viewName: ViewName,
params: PouchDB.Query.Options<T, T>,
db?: PouchDB.Database,
opts?: QueryViewOptions
): Promise<T[] | T | undefined> => {
const CreateFuncByName = {
[ViewName.USER_BY_EMAIL]: createNewUserEmailView,
[ViewName.BY_API_KEY]: createApiKeyView,
[ViewName.USER_BY_BUILDERS]: createUserBuildersView,
[ViewName.USER_BY_APP]: createUserAppView,
}
// can pass DB in if working with something specific
if (!db) {
db = getGlobalDB() as PouchDB.Database
}
return queryView(viewName, params, db, CreateFuncByName, opts)
}

View File

@ -37,7 +37,7 @@ const env = {
MULTI_TENANCY: process.env.MULTI_TENANCY, MULTI_TENANCY: process.env.MULTI_TENANCY,
ACCOUNT_PORTAL_URL: ACCOUNT_PORTAL_URL:
process.env.ACCOUNT_PORTAL_URL || "https://account.budibase.app", process.env.ACCOUNT_PORTAL_URL || "https://account.budibase.app",
ACCOUNT_PORTAL_API_KEY: process.env.ACCOUNT_PORTAL_API_KEY, ACCOUNT_PORTAL_API_KEY: process.env.ACCOUNT_PORTAL_API_KEY || "",
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL, DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED || ""), SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED || ""),
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN, COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,4 @@
import errors from "./errors" import errors from "./errors"
const errorClasses = errors.errors const errorClasses = errors.errors
import * as events from "./events" import * as events from "./events"
import * as migrations from "./migrations" import * as migrations from "./migrations"
@ -15,7 +14,7 @@ import deprovisioning from "./context/deprovision"
import auth from "./auth" import auth from "./auth"
import constants from "./constants" import constants from "./constants"
import * as dbConstants from "./db/constants" import * as dbConstants from "./db/constants"
import logging from "./logging" import * as logging from "./logging"
import pino from "./pino" import pino from "./pino"
import * as middleware from "./middleware" import * as middleware from "./middleware"
import plugins from "./plugin" import plugins from "./plugin"

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import { doWithDB } from "../db" import { doWithDB } from "../db"
import { StaticDatabases } from "../db/constants" import { queryPlatformView } from "../db/views"
import { baseGlobalDBName } from "./utils" import { StaticDatabases, ViewName } from "../db/constants"
import { getGlobalDBName } from "./utils"
import { import {
getTenantId, getTenantId,
DEFAULT_TENANT_ID, DEFAULT_TENANT_ID,
@ -8,6 +9,7 @@ import {
getTenantIDFromAppID, getTenantIDFromAppID,
} from "../context" } from "../context"
import env from "../environment" import env from "../environment"
import { PlatformUser, PlatformUserByEmail } from "@budibase/types"
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants
const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name
@ -87,15 +89,6 @@ export const tryAddTenant = async (
}) })
} }
export const getGlobalDBName = (tenantId?: string) => {
// tenant ID can be set externally, for example user API where
// new tenants are being created, this may be the case
if (!tenantId) {
tenantId = getTenantId()
}
return baseGlobalDBName(tenantId)
}
export const doWithGlobalDB = (tenantId: string, cb: any) => { export const doWithGlobalDB = (tenantId: string, cb: any) => {
return doWithDB(getGlobalDBName(tenantId), cb) return doWithDB(getGlobalDBName(tenantId), cb)
} }
@ -116,14 +109,16 @@ export const lookupTenantId = async (userId: string) => {
} }
// lookup, could be email or userId, either will return a doc // lookup, could be email or userId, either will return a doc
export const getTenantUser = async (identifier: string) => { export const getTenantUser = async (
return doWithDB(PLATFORM_INFO_DB, async (db: any) => { identifier: string
try { ): Promise<PlatformUser | null> => {
return await db.get(identifier) // use the view here and allow to find anyone regardless of casing
} catch (err) { // Use lowercase to ensure email login is case insensitive
return null const response = queryPlatformView(ViewName.PLATFORM_USERS_LOWERCASE, {
} keys: [identifier.toLowerCase()],
}) include_docs: true,
}) as Promise<PlatformUser>
return response
} }
export const isUserInAppTenant = (appId: string, user: any) => { export const isUserInAppTenant = (appId: string, user: any) => {

View File

@ -1,12 +0,0 @@
const { DEFAULT_TENANT_ID } = require("../constants")
const { StaticDatabases, SEPARATOR } = require("../db/constants")
exports.baseGlobalDBName = tenantId => {
let dbName
if (!tenantId || tenantId === DEFAULT_TENANT_ID) {
dbName = StaticDatabases.GLOBAL.name
} else {
dbName = `${tenantId}${SEPARATOR}${StaticDatabases.GLOBAL.name}`
}
return dbName
}

View File

@ -0,0 +1,22 @@
import { DEFAULT_TENANT_ID } from "../constants"
import { StaticDatabases, SEPARATOR } from "../db/constants"
import { getTenantId } from "../context"
export const getGlobalDBName = (tenantId?: string) => {
// tenant ID can be set externally, for example user API where
// new tenants are being created, this may be the case
if (!tenantId) {
tenantId = getTenantId()
}
return baseGlobalDBName(tenantId)
}
export const baseGlobalDBName = (tenantId: string | undefined | null) => {
let dbName
if (!tenantId || tenantId === DEFAULT_TENANT_ID) {
dbName = StaticDatabases.GLOBAL.name
} else {
dbName = `${tenantId}${SEPARATOR}${StaticDatabases.GLOBAL.name}`
}
return dbName
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -779,7 +779,7 @@ Cypress.Commands.add("navigateToAutogeneratedModal", () => {
Cypress.Commands.add("selectExternalDatasource", datasourceName => { Cypress.Commands.add("selectExternalDatasource", datasourceName => {
// Navigates to Data Section // Navigates to Data Section
cy.navigateToDataSection() cy.navigateToDataSection()
// Open Data Source modal // Open Datasource modal
cy.get(".nav").within(() => { cy.get(".nav").within(() => {
cy.get(".add-button").click() cy.get(".add-button").click()
}) })

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/builder", "name": "@budibase/builder",
"version": "1.3.15-alpha.7", "version": "1.3.19-alpha.6",
"license": "GPL-3.0", "license": "GPL-3.0",
"private": true, "private": true,
"scripts": { "scripts": {
@ -69,10 +69,10 @@
} }
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "1.3.15-alpha.7", "@budibase/bbui": "1.3.19-alpha.6",
"@budibase/client": "1.3.15-alpha.7", "@budibase/client": "1.3.19-alpha.6",
"@budibase/frontend-core": "1.3.15-alpha.7", "@budibase/frontend-core": "1.3.19-alpha.6",
"@budibase/string-templates": "1.3.15-alpha.7", "@budibase/string-templates": "1.3.19-alpha.6",
"@sentry/browser": "5.19.1", "@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1", "@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1", "@spectrum-css/vars": "^3.0.1",

View File

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

View File

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

View File

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

View File

@ -50,7 +50,6 @@
type="string" type="string"
{bindings} {bindings}
fillWidth={true} fillWidth={true}
allowJS={false}
/> />
{/each} {/each}
</div> </div>

View File

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

View File

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

View File

@ -71,13 +71,13 @@
<div class="root"> <div class="root">
<Body size="S"> <Body size="S">
Choosing a Data Source will automatically use the data it provides, but it's Choosing a Datasource will automatically use the data it provides, but it's
optional.<br /> optional.<br />
You can always add or override fields manually. You can always add or override fields manually.
</Body> </Body>
<div class="params"> <div class="params">
<Label small>Data Source</Label> <Label small>Datasource</Label>
<Select <Select
bind:value={parameters.providerId} bind:value={parameters.providerId}
options={providerOptions} options={providerOptions}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,84 @@
<script>
import { Modal, ModalContent, Body, TooltipWrapper } from "@budibase/bbui"
import { auth, admin, licensing } from "stores/portal"
import { onMount } from "svelte"
export let onDismiss = () => {}
export let onShow = () => {}
let paymentFailedModal
let pastDueEndDate
const paymentFailedTitle = "Payment failed"
$: accountUrl = $admin.accountPortalUrl
$: upgradeUrl = `${accountUrl}/portal/upgrade`
export function show() {
paymentFailedModal.show()
}
export function hide() {
paymentFailedModal.hide()
}
onMount(() => {
licensing.subscribe(state => {
pastDueEndDate = state.pastDueEndDate
})
})
</script>
<Modal bind:this={paymentFailedModal} on:show={onShow} on:hide={onDismiss}>
{#if $auth.user.accountPortalAccess}
<ModalContent
title={paymentFailedTitle}
size="M"
confirmText="Upgrade"
onConfirm={() => {
window.location.href = upgradeUrl
}}
>
<Body>The payment for your subscription has failed</Body>
<Body>
Please upgrade your billing details before your account gets downgraded
to the free plan
</Body>
<Body weight={800}>
<div class="tooltip-root">
{`${$licensing.pastDueDaysRemaining} day${
$licensing.pastDueDaysRemaining == 1 ? "" : "s"
} remaining`}
<span class="tooltip">
<TooltipWrapper tooltip={pastDueEndDate} size="S" />
</span>
</div>
</Body>
</ModalContent>
{:else}
<ModalContent title={paymentFailedTitle} size="M" showCancelButton={false}>
<Body>The payment for your subscription has failed</Body>
<Body>
Please upgrade your billing details before your account gets downgraded
to the free plan
</Body>
<Body>Please contact your account holder.</Body>
<Body weight={800}>
<div class="tooltip-root">
{`${$licensing.pastDueDaysRemaining} day${
$licensing.pastDueDaysRemaining == 1 ? "" : "s"
} remaining`}
<span class="tooltip">
<TooltipWrapper tooltip={pastDueEndDate} size="S" />
</span>
</div>
</Body>
</ModalContent>
{/if}
</Modal>
<style>
.tooltip-root {
display: flex;
align-items: center;
}
</style>

View File

@ -0,0 +1,15 @@
export const ExpiringKeys = {
LICENSING_DAYPASS_WARNING_MODAL: "licensing_daypass_warning_90_modal",
LICENSING_DAYPASS_WARNING_BANNER: "licensing_daypass_warning_90_banner",
LICENSING_PAYMENT_FAILED: "licensing_payment_failed",
LICENSING_ACCOUNT_DOWNGRADED_MODAL: "licensing_account_downgraded_modal",
LICENSING_APP_LIMIT_MODAL: "licensing_app_limit_modal",
LICENSING_ROWS_WARNING_BANNER: "licensing_rows_warning_banner",
LICENSING_AUTOMATIONS_WARNING_BANNER: "licensing_automations_warning_banner",
LICENSING_QUERIES_WARNING_BANNER: "licensing_queries_warning_banner",
}
export const StripeStatus = {
PAST_DUE: "past_due",
ACTIVE: "active",
}

View File

@ -0,0 +1,156 @@
import { ExpiringKeys } from "./constants"
import { temporalStore } from "builderStore"
import { admin, auth, licensing } from "stores/portal"
import { get } from "svelte/store"
import { BANNER_TYPES } from "@budibase/bbui"
const oneDayInSeconds = 86400
const defaultCacheFn = key => {
temporalStore.actions.setExpiring(key, {}, oneDayInSeconds)
}
const defaultAction = key => {
if (!get(auth).user.accountPortalAccess) {
return {}
}
return {
extraButtonText: "Upgrade Plan",
extraButtonAction: () => {
defaultCacheFn(key)
window.location.href = `${get(admin).accountPortalUrl}/portal/upgrade`
},
}
}
const buildUsageInfoBanner = (
metricKey,
metricLabel,
cacheKey,
percentageThreshold,
customMessage
) => {
const appAuth = get(auth)
const appLicensing = get(licensing)
const displayPercent =
appLicensing?.usageMetrics[metricKey] > 100
? 100
: appLicensing?.usageMetrics[metricKey]
let bannerConfig = {
key: cacheKey,
type: BANNER_TYPES.INFO,
onChange: () => {
defaultCacheFn(cacheKey)
},
message: customMessage
? customMessage
: `You have used ${displayPercent}% of your monthly usage of ${metricLabel} with ${
appLicensing.quotaResetDaysRemaining
} day${
appLicensing.quotaResetDaysRemaining == 1 ? "" : "s"
} remaining. ${
appAuth.user.accountPortalAccess
? ""
: "Please contact your account holder to upgrade"
}`,
criteria: () => {
return appLicensing?.usageMetrics[metricKey] >= percentageThreshold
},
tooltip: appLicensing?.quotaResetDate,
}
return !get(auth).user.accountPortalAccess
? bannerConfig
: {
...bannerConfig,
...defaultAction(cacheKey),
}
}
const buildDayPassBanner = () => {
const appAuth = get(auth)
const appLicensing = get(licensing)
if (get(licensing)?.usageMetrics["dayPasses"] >= 100) {
return {
key: "max_dayPasses",
type: BANNER_TYPES.NEGATIVE,
criteria: () => {
return true
},
message: `Your apps are currently offline. You have exceeded your plans limit for Day Passes. ${
appAuth.user.accountPortalAccess
? ""
: "Please contact your account holder to upgrade."
}`,
...defaultAction(),
showCloseButton: false,
}
}
return buildUsageInfoBanner(
"dayPasses",
"Day Passes",
ExpiringKeys.LICENSING_DAYPASS_WARNING_BANNER,
90,
`You have used ${
appLicensing?.usageMetrics["dayPasses"]
}% of your monthly usage of Day Passes with ${
appLicensing?.quotaResetDaysRemaining
} day${
get(licensing).quotaResetDaysRemaining == 1 ? "" : "s"
} remaining. All apps will be taken offline if this limit is reached. ${
appAuth.user.accountPortalAccess
? ""
: "Please contact your account holder to upgrade."
}`
)
}
const buildPaymentFailedBanner = () => {
return {
key: "payment_Failed",
type: BANNER_TYPES.NEGATIVE,
criteria: () => {
return get(licensing)?.accountPastDue && !get(licensing).isFreePlan()
},
message: `Payment Failed - Please update your billing details or your account will be downgrades in
${get(licensing)?.pastDueDaysRemaining} day${
get(licensing)?.pastDueDaysRemaining == 1 ? "" : "s"
}`,
...defaultAction(),
showCloseButton: false,
tooltip: get(licensing).pastDueEndDate,
}
}
export const getBanners = () => {
return [
buildPaymentFailedBanner(),
buildDayPassBanner(ExpiringKeys.LICENSING_DAYPASS_WARNING_BANNER),
buildUsageInfoBanner(
"rows",
"Rows",
ExpiringKeys.LICENSING_ROWS_WARNING_BANNER,
90
),
buildUsageInfoBanner(
"automations",
"Automations",
ExpiringKeys.LICENSING_AUTOMATIONS_WARNING_BANNER,
90
),
buildUsageInfoBanner(
"queries",
"Queries",
ExpiringKeys.LICENSING_QUERIES_WARNING_BANNER,
90
),
].filter(licensingBanner => {
return (
!temporalStore.actions.getExpiring(licensingBanner.key) &&
licensingBanner.criteria()
)
})
}

View File

@ -1,10 +1,15 @@
<script> <script>
import { Body, ProgressBar, Label } from "@budibase/bbui" import { Body, ProgressBar, Heading, Icon, Link } from "@budibase/bbui"
import { admin, auth } from "../../stores/portal"
import { onMount } from "svelte" import { onMount } from "svelte"
export let usage export let usage
export let warnWhenFull = false
let percentage let percentage
let unlimited = false let unlimited = false
let showWarning = false
$: accountPortalAccess = $auth?.user?.accountPortalAccess
const isUnlimited = () => { const isUnlimited = () => {
if (usage.total === -1) { if (usage.total === -1) {
@ -14,29 +19,62 @@
} }
const getPercentage = () => { const getPercentage = () => {
return Math.min(Math.ceil((usage.used / usage.total) * 100), 100) return (usage.used / usage.total) * 100
} }
const upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade`
onMount(() => { onMount(() => {
unlimited = isUnlimited() unlimited = isUnlimited()
percentage = getPercentage() percentage = getPercentage()
if (warnWhenFull && percentage === 100) {
showWarning = true
}
}) })
</script> </script>
<div class="usage"> <div class="usage">
<div class="info"> <div class="info">
<Label size="XL">{usage.name}</Label> <div class="header-container">
{#if showWarning}
<Icon name="Alert" />
{/if}
<div class="heading header-item">
<Heading size="XS" weight="light">{usage.name}</Heading>
</div>
</div>
{#if unlimited} {#if unlimited}
<Body size="S">{usage.used}</Body> <Body size="S">{usage.used} / Unlimited</Body>
{:else} {:else}
<Body size="S">{usage.used} / {usage.total}</Body> <Body size="S">{usage.used} / {usage.total}</Body>
{/if} {/if}
</div> </div>
<div> <div>
{#if unlimited} {#if unlimited}
<Body size="S">Unlimited</Body> <ProgressBar
showPercentage={false}
width={"100%"}
duration={1}
value={100}
/>
{:else} {:else}
<ProgressBar width={"100%"} duration={1} value={percentage} /> <ProgressBar
color={showWarning ? "red" : "green"}
showPercentage={false}
width={"100%"}
duration={1}
value={percentage}
/>
{/if}
{#if showWarning}
<Body size="S">
To get more {usage.name.toLowerCase()}
{#if accountPortalAccess}
<Link href={upgradeUrl}>upgrade your plan</Link>
{:else}
contact your account holder
{/if}
</Body>
{/if} {/if}
</div> </div>
</div> </div>
@ -51,6 +89,13 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
gap: var(--spacing-m); margin-bottom: 12px;
}
.header-container {
display: flex;
}
.heading {
margin-top: 3px;
margin-left: 5px;
} }
</style> </style>

View File

@ -0,0 +1,119 @@
<script>
import {
Detail,
Button,
Heading,
Layout,
Body,
TooltipWrapper,
} from "@budibase/bbui"
export let description = ""
export let title = ""
export let primaryAction
export let secondaryAction
export let primaryActionText
export let secondaryActionText
export let primaryCta = true
export let textRows = []
$: primaryDefined = primaryAction && primaryActionText
$: secondaryDefined = secondaryAction && secondaryActionText
</script>
<div class="dash-card">
<div class="dash-card-header">
<div class="header-info">
<Layout gap="XS">
<div class="dash-card-title">
<Detail size="M">{description}</Detail>
</div>
<Heading size="M">{title}</Heading>
{#if textRows.length}
<div class="text-rows">
{#each textRows as row}
{#if row.tooltip}
<TooltipWrapper tooltip={row.tooltip}>
<Body>{row.message}</Body>
</TooltipWrapper>
{:else}
<Body>{row.message}</Body>
{/if}
{/each}
</div>
{/if}
</Layout>
</div>
<div class="header-actions">
{#if secondaryDefined}
<div>
<Button newStyles secondary on:click={secondaryAction}
>{secondaryActionText}</Button
>
</div>
{/if}
{#if primaryDefined}
<div class="primary-button">
<Button cta={primaryCta} on:click={primaryAction}
>{primaryActionText}</Button
>
</div>
{/if}
</div>
</div>
<div class="dash-card-body">
<slot />
</div>
</div>
<style>
.dash-card {
background: var(--spectrum-alias-background-color-primary);
border-radius: var(--border-radius-s);
overflow: hidden;
min-height: 150px;
}
.dash-card-header {
padding: 15px 25px 20px;
border-bottom: 1px solid var(--spectrum-global-color-gray-300);
display: flex;
flex-direction: row;
justify-content: space-between;
}
.dash-card-body {
padding: 25px 30px;
}
.dash-card-title :global(.spectrum-Detail) {
color: var(
--spectrum-sidenav-heading-text-color,
var(--spectrum-global-color-gray-700)
);
display: inline-block;
}
.header-info {
flex: 1;
}
.header-actions {
flex: 1;
margin-top: 15px;
display: flex;
flex-direction: row;
justify-content: flex-end;
}
.header-actions :global(:first-child) {
margin-right: 5px;
}
.text-rows {
margin-top: 10px;
}
@media only screen and (max-width: 900px) {
.dash-card-header {
flex-direction: column;
}
.header-actions {
justify-content: flex-start "bul";
}
}
</style>

View File

@ -0,0 +1,2 @@
export { default as Usage } from "./Usage.svelte"
export { default as DashCard } from "./UsageDashCard.svelte"

View File

@ -58,6 +58,13 @@ export const DefaultAppTheme = {
navTextColor: "var(--spectrum-global-color-gray-800)", navTextColor: "var(--spectrum-global-color-gray-800)",
} }
export const PlanType = {
FREE: "free",
PRO: "pro",
BUSINESS: "business",
ENTERPRISE: "enterprise",
}
export const PluginSource = { export const PluginSource = {
URL: "URL", URL: "URL",
NPM: "NPM", NPM: "NPM",

View File

@ -1,6 +1,6 @@
<script> <script>
import { isActive, redirect, params } from "@roxi/routify" import { isActive, redirect, params } from "@roxi/routify"
import { admin, auth } from "stores/portal" import { admin, auth, licensing } from "stores/portal"
import { onMount } from "svelte" import { onMount } from "svelte"
import { CookieUtils, Constants } from "@budibase/frontend-core" import { CookieUtils, Constants } from "@budibase/frontend-core"
import { API } from "api" import { API } from "api"
@ -63,6 +63,10 @@
await auth.getSelf() await auth.getSelf()
await admin.init() await admin.init()
if ($auth.user) {
await licensing.init()
}
// Set init info if present // Set init info if present
if ($params["?template"]) { if ($params["?template"]) {
await auth.setInitInfo({ init_template: $params["?template"] }) await auth.setInitInfo({ init_template: $params["?template"] })

View File

@ -23,7 +23,8 @@
"dataprovider", "dataprovider",
"repeater", "repeater",
"table", "table",
"dynamicfilter" "dynamicfilter",
"daterangepicker"
] ]
}, },
{ {
@ -60,7 +61,6 @@
"booleanfield", "booleanfield",
"longformfield", "longformfield",
"attachmentfield", "attachmentfield",
"daterangepicker",
"jsonfield", "jsonfield",
"relationshipfield", "relationshipfield",
"datetimefield", "datetimefield",
@ -81,4 +81,3 @@
] ]
} }
] ]

View File

@ -13,13 +13,14 @@
notifications, notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import { onMount } from "svelte" import { onMount } from "svelte"
import { apps, organisation, auth, groups } from "stores/portal" import { apps, organisation, auth, groups, licensing } from "stores/portal"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { AppStatus } from "constants" import { AppStatus } from "constants"
import { gradient } from "actions" import { gradient } from "actions"
import UpdateUserInfoModal from "components/settings/UpdateUserInfoModal.svelte" import UpdateUserInfoModal from "components/settings/UpdateUserInfoModal.svelte"
import ChangePasswordModal from "components/settings/ChangePasswordModal.svelte" import ChangePasswordModal from "components/settings/ChangePasswordModal.svelte"
import { processStringSync } from "@budibase/string-templates" import { processStringSync } from "@budibase/string-templates"
import Spaceman from "assets/bb-space-man.svg"
import Logo from "assets/bb-emblem.svg" import Logo from "assets/bb-emblem.svg"
let loaded = false let loaded = false
@ -91,7 +92,7 @@
<div class="content"> <div class="content">
<Layout noPadding> <Layout noPadding>
<div class="header"> <div class="header">
<img alt="logo" src={$organisation.logoUrl || Logo} /> <img class="logo" alt="logo" src={$organisation.logoUrl || Logo} />
<ActionMenu align="right" dataCy="user-menu"> <ActionMenu align="right" dataCy="user-menu">
<div slot="control" class="avatar"> <div slot="control" class="avatar">
<Avatar <Avatar
@ -131,7 +132,17 @@
</Body> </Body>
</Layout> </Layout>
<Divider /> <Divider />
{#if userApps.length} {#if $licensing.usageMetrics.dayPasses >= 100}
<div>
<Layout gap="S" justifyItems="center">
<img class="spaceman" alt="spaceman" src={Spaceman} />
<Heading size="M">
{"Your apps are currently offline."}
</Heading>
Please contact the account holder to get them back online.
</Layout>
</div>
{:else if userApps.length}
<Heading>Apps</Heading> <Heading>Apps</Heading>
<div class="group"> <div class="group">
<Layout gap="S" noPadding> <Layout gap="S" noPadding>
@ -194,10 +205,13 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
} }
img { img.logo {
width: 40px; width: 40px;
margin-bottom: -12px; margin-bottom: -12px;
} }
img.spaceman {
width: 100px;
}
.avatar { .avatar {
display: grid; display: grid;
grid-template-columns: auto auto; grid-template-columns: auto auto;

View File

@ -13,8 +13,7 @@
notifications, notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import ConfigChecklist from "components/common/ConfigChecklist.svelte" import ConfigChecklist from "components/common/ConfigChecklist.svelte"
import { organisation, auth } from "stores/portal" import { organisation, auth, admin as adminStore } from "stores/portal"
import { admin as adminStore } from "stores/portal"
import { onMount } from "svelte" import { onMount } from "svelte"
import UpdateUserInfoModal from "components/settings/UpdateUserInfoModal.svelte" import UpdateUserInfoModal from "components/settings/UpdateUserInfoModal.svelte"
import ChangePasswordModal from "components/settings/ChangePasswordModal.svelte" import ChangePasswordModal from "components/settings/ChangePasswordModal.svelte"
@ -37,14 +36,6 @@
href: "/builder/portal/apps", href: "/builder/portal/apps",
}, },
] ]
if (isEnabled(FEATURE_FLAGS.LICENSING)) {
menu = menu.concat([
{
title: "Usage",
href: "/builder/portal/settings/usage",
},
])
}
if (admin) { if (admin) {
menu = menu.concat([ menu = menu.concat([
@ -53,9 +44,20 @@
href: "/builder/portal/manage/users", href: "/builder/portal/manage/users",
heading: "Manage", heading: "Manage",
}, },
isEnabled(FEATURE_FLAGS.USER_GROUPS)
? {
title: "User Groups",
href: "/builder/portal/manage/groups",
badge: "New",
}
: undefined,
{ title: "Auth", href: "/builder/portal/manage/auth" }, { title: "Auth", href: "/builder/portal/manage/auth" },
{ title: "Email", href: "/builder/portal/manage/email" }, { title: "Email", href: "/builder/portal/manage/email" },
{ title: "Plugins", href: "/builder/portal/manage/plugins" }, {
title: "Plugins",
href: "/builder/portal/manage/plugins",
badge: "New",
},
{ {
title: "Organisation", title: "Organisation",
@ -68,15 +70,6 @@
}, },
]) ])
if (isEnabled(FEATURE_FLAGS.USER_GROUPS)) {
let item = {
title: "User Groups",
href: "/builder/portal/manage/groups",
}
menu.splice(2, 0, item)
}
if (!$adminStore.cloud) { if (!$adminStore.cloud) {
menu = menu.concat([ menu = menu.concat([
{ {
@ -84,13 +77,6 @@
href: "/builder/portal/settings/update", href: "/builder/portal/settings/update",
}, },
]) ])
if (isEnabled(FEATURE_FLAGS.LICENSING)) {
menu = menu.concat({
title: "Upgrade",
href: "/builder/portal/settings/upgrade",
})
}
} }
} else { } else {
menu = menu.concat([ menu = menu.concat([
@ -103,14 +89,62 @@
} }
// add link to account portal if the user has access // add link to account portal if the user has access
if ($auth?.user?.accountPortalAccess) { let accountSectionAdded = false
// link out to account-portal if account holder in cloud or always in self-host
if ($auth?.user?.accountPortalAccess || (!$adminStore.cloud && admin)) {
accountSectionAdded = true
menu = menu.concat([ menu = menu.concat([
{ {
title: "Account", title: "Account",
href: $adminStore.accountPortalUrl, href: $adminStore.accountPortalUrl,
heading: "Account",
}, },
]) ])
} }
if (isEnabled(FEATURE_FLAGS.LICENSING)) {
// always show usage in self-host or cloud if licensing enabled
menu = menu.concat([
{
title: "Usage",
href: "/builder/portal/settings/usage",
heading: accountSectionAdded ? "" : "Account",
},
])
// show the relevant hosting upgrade page
if ($adminStore.cloud && $auth?.user?.accountPortalAccess) {
menu = menu.concat([
{
title: "Upgrade",
href: $adminStore.accountPortalUrl + "/portal/upgrade",
badge: "New",
},
])
} else if (!$adminStore.cloud && admin) {
menu = menu.concat({
title: "Upgrade",
href: "/builder/portal/settings/upgrade",
badge: "New",
})
}
// show the billing page to licensed account holders in cloud
if (
$auth?.user?.accountPortalAccess &&
$auth.user.account.stripeCustomerId
) {
menu = menu.concat([
{
title: "Billing",
href: $adminStore.accountPortalUrl + "/portal/billing",
},
])
}
}
menu = menu.filter(item => !!item)
return menu return menu
} }
@ -161,11 +195,12 @@
</div> </div>
<div class="menu"> <div class="menu">
<Navigation> <Navigation>
{#each menu as { title, href, heading }} {#each menu as { title, href, heading, badge }}
<Item <Item
on:click={hideMobileMenu} on:click={hideMobileMenu}
selected={$isActive(href)} selected={$isActive(href)}
{href} {href}
{badge}
{heading}>{title}</Item {heading}>{title}</Item
> >
{/each} {/each}

View File

@ -13,12 +13,14 @@
} from "@budibase/bbui" } from "@budibase/bbui"
import CreateAppModal from "components/start/CreateAppModal.svelte" import CreateAppModal from "components/start/CreateAppModal.svelte"
import TemplateDisplay from "components/common/TemplateDisplay.svelte" import TemplateDisplay from "components/common/TemplateDisplay.svelte"
import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte"
import { onMount } from "svelte" import { onMount } from "svelte"
import { templates } from "stores/portal" import { templates, licensing } from "stores/portal"
let loaded = $templates?.length let loaded = $templates?.length
let template let template
let creationModal = false let creationModal = false
let appLimitModal
let creatingApp = false let creatingApp = false
const welcomeBody = const welcomeBody =
@ -29,6 +31,8 @@
onMount(async () => { onMount(async () => {
try { try {
await templates.load() await templates.load()
await licensing.getQuotaUsage()
await licensing.getUsageMetrics()
if ($templates?.length === 0) { if ($templates?.length === 0) {
notifications.error( notifications.error(
"There was a problem loading quick start templates." "There was a problem loading quick start templates."
@ -41,10 +45,14 @@
}) })
const initiateAppCreation = () => { const initiateAppCreation = () => {
if ($licensing.usageMetrics.apps >= 100) {
appLimitModal.show()
} else {
template = null template = null
creationModal.show() creationModal.show()
creatingApp = true creatingApp = true
} }
}
const stopAppCreation = () => { const stopAppCreation = () => {
template = null template = null
@ -52,10 +60,14 @@
} }
const initiateAppImport = () => { const initiateAppImport = () => {
if ($licensing.usageMetrics.apps >= 100) {
appLimitModal.show()
} else {
template = { fromFile: true } template = { fromFile: true }
creationModal.show() creationModal.show()
creatingApp = true creatingApp = true
} }
}
</script> </script>
<Page wide> <Page wide>
@ -121,6 +133,7 @@
> >
<CreateAppModal {template} /> <CreateAppModal {template} />
</Modal> </Modal>
<AppLimitModal bind:this={appLimitModal} />
<style> <style>
.title .welcome > .buttons { .title .welcome > .buttons {

View File

@ -15,11 +15,12 @@
import Spinner from "components/common/Spinner.svelte" import Spinner from "components/common/Spinner.svelte"
import CreateAppModal from "components/start/CreateAppModal.svelte" import CreateAppModal from "components/start/CreateAppModal.svelte"
import UpdateAppModal from "components/start/UpdateAppModal.svelte" import UpdateAppModal from "components/start/UpdateAppModal.svelte"
import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte"
import { store, automationStore } from "builderStore" import { store, automationStore } from "builderStore"
import { API } from "api" import { API } from "api"
import { onMount } from "svelte" import { onMount } from "svelte"
import { apps, auth, admin, templates } from "stores/portal" import { apps, auth, admin, templates, licensing } from "stores/portal"
import download from "downloadjs" import download from "downloadjs"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import AppRow from "components/start/AppRow.svelte" import AppRow from "components/start/AppRow.svelte"
@ -32,6 +33,7 @@
let selectedApp let selectedApp
let creationModal let creationModal
let updatingModal let updatingModal
let appLimitModal
let creatingApp = false let creatingApp = false
let loaded = $apps?.length || $templates?.length let loaded = $apps?.length || $templates?.length
let searchTerm = "" let searchTerm = ""
@ -124,8 +126,10 @@
return `${app.name} - Automation error (${errorCount(errors)})` return `${app.name} - Automation error (${errorCount(errors)})`
} }
const initiateAppCreation = () => { const initiateAppCreation = async () => {
if ($apps?.length) { if ($licensing.usageMetrics.apps >= 100) {
appLimitModal.show()
} else if ($apps?.length) {
$goto("/builder/portal/apps/create") $goto("/builder/portal/apps/create")
} else { } else {
template = null template = null
@ -225,6 +229,10 @@
try { try {
await apps.load() await apps.load()
await templates.load() await templates.load()
await licensing.getQuotaUsage()
await licensing.getUsageMetrics()
if ($templates?.length === 0) { if ($templates?.length === 0) {
notifications.error( notifications.error(
"There was a problem loading quick start templates." "There was a problem loading quick start templates."
@ -405,6 +413,8 @@
<UpdateAppModal app={selectedApp} /> <UpdateAppModal app={selectedApp} />
</Modal> </Modal>
<AppLimitModal bind:this={appLimitModal} />
<style> <style>
.appTable { .appTable {
border-top: var(--border-light); border-top: var(--border-light);

View File

@ -38,9 +38,13 @@
try { try {
await groups.actions.save(group) await groups.actions.save(group)
} catch (error) { } catch (error) {
if (error.status === 400) {
notifications.error(error.message)
} else {
notifications.error(`Failed to save group`) notifications.error(`Failed to save group`)
} }
} }
}
const showCreateGroupModal = () => { const showCreateGroupModal = () => {
group = cloneDeep(DefaultGroup) group = cloneDeep(DefaultGroup)

View File

@ -20,6 +20,8 @@
? plugin.schema.schema.icon || "Book" ? plugin.schema.schema.icon || "Book"
: plugin.schema.schema.icon || "Beaker" : plugin.schema.schema.icon || "Beaker"
$: friendlyName = plugin?.schema?.schema?.friendlyName
function pluginDeleted() { function pluginDeleted() {
if (detailsModal) { if (detailsModal) {
detailsModal.hide() detailsModal.hide()
@ -41,6 +43,9 @@
> >
{plugin.name} {plugin.name}
</Body> </Body>
<Body size="XS" color="var(--spectrum-global-color-gray-900)">
{friendlyName}
</Body>
</div> </div>
</div> </div>
</div> </div>
@ -65,6 +70,11 @@
<Input disabled value={plugin.name} /> <Input disabled value={plugin.name} />
</div> </div>
<div class="details-row">
<Label size="M">Friendly name</Label>
<Input disabled value={friendlyName} />
</div>
<div class="details-row"> <div class="details-row">
<Label size="M">Type</Label> <Label size="M">Type</Label>
<Input <Input
@ -127,7 +137,7 @@
.details-row { .details-row {
display: grid; display: grid;
grid-template-columns: 60px 1fr; grid-template-columns: 70px 1fr;
grid-gap: var(--spacing-l) var(--spacing-l); grid-gap: var(--spacing-l) var(--spacing-l);
align-items: center; align-items: center;
} }

View File

@ -88,14 +88,14 @@
<Heading size="S">Information</Heading> <Heading size="S">Information</Heading>
<Body size="S">Here you can update your logo and organization name.</Body> <Body size="S">Here you can update your logo and organization name.</Body>
</Layout> </Layout>
<div className="fields"> <div class="fields">
<div className="field"> <div class="field">
<Label size="L">Org. name</Label> <Label size="L">Org. name</Label>
<Input thin bind:value={$values.company} /> <Input thin bind:value={$values.company} />
</div> </div>
<div className="field logo"> <div class="field logo">
<Label size="L">Logo</Label> <Label size="L">Logo</Label>
<div className="file"> <div class="file">
<Dropzone <Dropzone
value={[$values.logo]} value={[$values.logo]}
on:change={e => { on:change={e => {
@ -115,13 +115,14 @@
<Heading size="S">Platform</Heading> <Heading size="S">Platform</Heading>
<Body size="S">Here you can set up general platform settings.</Body> <Body size="S">Here you can set up general platform settings.</Body>
</Layout> </Layout>
<div className="fields"> <div class="fields">
<div className="field"> <div class="field">
<Label <Label
size="L" size="L"
tooltip={"Update the Platform URL to match your Budibase web URL. This keeps email templates and authentication configs up to date."} tooltip={"Update the Platform URL to match your Budibase web URL. This keeps email templates and authentication configs up to date."}
>Platform URL</Label
> >
Platform URL
</Label>
<Input thin bind:value={$values.platformUrl} /> <Input thin bind:value={$values.platformUrl} />
</div> </div>
</div> </div>

View File

@ -35,10 +35,14 @@
} }
const activate = async () => { const activate = async () => {
try {
await API.activateLicenseKey({ licenseKey }) await API.activateLicenseKey({ licenseKey })
await auth.getSelf() await auth.getSelf()
await setLicenseInfo() await setLicenseInfo()
notifications.success("Successfully activated") notifications.success("Successfully activated")
} catch (e) {
notifications.error(e.message)
}
} }
const refresh = async () => { const refresh = async () => {

View File

@ -5,49 +5,74 @@
Heading, Heading,
Layout, Layout,
notifications, notifications,
Detail,
Link, Link,
TooltipWrapper,
} from "@budibase/bbui" } from "@budibase/bbui"
import { onMount } from "svelte" import { onMount } from "svelte"
import { admin, auth, licensing } from "stores/portal" import { admin, auth, licensing } from "../../../../stores/portal"
import Usage from "components/usage/Usage.svelte" import { PlanType } from "../../../../constants"
import { DashCard, Usage } from "../../../../components/usage"
let staticUsage = [] let staticUsage = []
let monthlyUsage = [] let monthlyUsage = []
let cancelAt
let loaded = false let loaded = false
let textRows = []
let daysRemainingInMonth
let primaryActionText
const upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade`
const manageUrl = `${$admin.accountPortalUrl}/portal/billing`
const WARN_USAGE = ["Queries", "Automations", "Rows", "Day Passes"]
const EXCLUDE_QUOTAS = ["Queries"]
$: quotaUsage = $licensing.quotaUsage $: quotaUsage = $licensing.quotaUsage
$: license = $auth.user?.license $: license = $auth.user?.license
$: accountPortalAccess = $auth?.user?.accountPortalAccess
const upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade` $: quotaReset = quotaUsage?.quotaReset
const setMonthlyUsage = () => { const setMonthlyUsage = () => {
monthlyUsage = [] monthlyUsage = []
if (quotaUsage.monthly) { if (quotaUsage.monthly) {
for (let [key, value] of Object.entries(license.quotas.usage.monthly)) { for (let [key, value] of Object.entries(license.quotas.usage.monthly)) {
if (EXCLUDE_QUOTAS.includes(value.name)) {
continue
}
const used = quotaUsage.monthly.current[key] const used = quotaUsage.monthly.current[key]
if (used !== undefined) { if (value.value !== 0) {
monthlyUsage.push({ monthlyUsage.push({
name: value.name, name: value.name,
used: used, used: used ? used : 0,
total: value.value, total: value.value,
}) })
} }
} }
} }
monthlyUsage = monthlyUsage.sort((a, b) => a.name.localeCompare(b.name))
} }
const setStaticUsage = () => { const setStaticUsage = () => {
staticUsage = [] staticUsage = []
for (let [key, value] of Object.entries(license.quotas.usage.static)) { for (let [key, value] of Object.entries(license.quotas.usage.static)) {
if (EXCLUDE_QUOTAS.includes(value.name)) {
continue
}
const used = quotaUsage.usageQuota[key] const used = quotaUsage.usageQuota[key]
if (used !== undefined) { if (value.value !== 0) {
staticUsage.push({ staticUsage.push({
name: value.name, name: value.name,
used: used, used: used ? used : 0,
total: value.value, total: value.value,
}) })
} }
} }
staticUsage = staticUsage.sort((a, b) => a.name.localeCompare(b.name))
}
const setCancelAt = () => {
cancelAt = license?.billing?.subscription?.cancelAt
} }
const capitalise = string => { const capitalise = string => {
@ -56,6 +81,70 @@
} }
} }
const planTitle = () => {
return capitalise(license?.plan.type)
}
const getDaysRemaining = timestamp => {
if (!timestamp) {
return
}
const now = new Date()
now.setHours(0)
now.setMinutes(0)
const thenDate = new Date(timestamp)
thenDate.setHours(0)
thenDate.setMinutes(0)
const difference = thenDate.getTime() - now
// return the difference in days
return (difference / (1000 * 3600 * 24)).toFixed(0)
}
const setTextRows = () => {
textRows = []
if (cancelAt) {
textRows.push({ message: "Subscription has been cancelled" })
textRows.push({
message: `${getDaysRemaining(cancelAt * 1000)} days remaining`,
tooltip: new Date(cancelAt * 1000),
})
}
}
const setDaysRemainingInMonth = () => {
const resetDate = new Date(quotaReset)
const now = new Date()
const difference = resetDate.getTime() - now.getTime()
// return the difference in days
daysRemainingInMonth = (difference / (1000 * 3600 * 24)).toFixed(0)
}
const goToAccountPortal = () => {
if (license?.plan.type === PlanType.FREE) {
window.location.href = upgradeUrl
} else {
window.location.href = manageUrl
}
}
const setPrimaryActionText = () => {
if (license?.plan.type === PlanType.FREE) {
primaryActionText = "Upgrade"
return
}
if (cancelAt) {
primaryActionText = "Renew"
} else {
primaryActionText = "Manage"
}
}
const init = async () => { const init = async () => {
try { try {
await licensing.getQuotaUsage() await licensing.getQuotaUsage()
@ -71,69 +160,98 @@
}) })
$: { $: {
if (license && quotaUsage) { if (license) {
setPrimaryActionText()
setCancelAt()
setTextRows()
setDaysRemainingInMonth()
if (quotaUsage) {
setMonthlyUsage() setMonthlyUsage()
setStaticUsage() setStaticUsage()
} }
} }
}
</script> </script>
{#if loaded} {#if loaded}
<Layout> <Layout>
<Layout noPadding gap="S">
<Heading>Usage</Heading> <Heading>Usage</Heading>
<Body <Body
>Get information about your current usage within Budibase. >Get information about your current usage within Budibase.
{#if $admin.cloud} {#if accountPortalAccess}
{#if $auth.user?.accountPortalAccess}
To upgrade your plan and usage limits visit your <Link To upgrade your plan and usage limits visit your <Link
size="L" on:click={goToAccountPortal}
href={upgradeUrl}>Account</Link size="L">Account</Link
>. >
{:else} {:else}
Contact your account holder to upgrade your usage limits. To upgrade your plan and usage limits contact your account holder
{/if}
{/if} {/if}
</Body> </Body>
</Layout> </Layout>
<Layout gap="S"> <Divider />
<Divider size="S" /> <DashCard
</Layout> description="YOUR CURRENT PLAN"
title={planTitle()}
{primaryActionText}
primaryAction={accountPortalAccess ? goToAccountPortal : undefined}
{textRows}
>
<Layout gap="S" noPadding> <Layout gap="S" noPadding>
<Layout gap="XS">
<Body size="S">YOUR PLAN</Body>
<Heading size="S">{capitalise(license?.plan.type)}</Heading>
</Layout>
<Layout gap="S"> <Layout gap="S">
<Body size="S">USAGE</Body>
<div class="usages"> <div class="usages">
<Layout noPadding>
{#each staticUsage as usage} {#each staticUsage as usage}
<div class="usage"> <div class="usage">
<Usage {usage} /> <Usage
{usage}
warnWhenFull={WARN_USAGE.includes(usage.name)}
/>
</div> </div>
{/each} {/each}
</Layout>
</div> </div>
</Layout> </Layout>
{#if monthlyUsage.length} {#if monthlyUsage.length}
<div class="monthly-container">
<Layout gap="S"> <Layout gap="S">
<Body size="S">MONTHLY</Body> <Heading size="S" weight="light">Monthly</Heading>
<div class="detail">
<TooltipWrapper tooltip={new Date(quotaReset)}>
<Detail size="M">Resets in {daysRemainingInMonth} days</Detail
>
</TooltipWrapper>
</div>
<div class="usages"> <div class="usages">
<Layout noPadding>
{#each monthlyUsage as usage} {#each monthlyUsage as usage}
<div class="usage"> <div class="usage">
<Usage {usage} /> <Usage
{usage}
warnWhenFull={WARN_USAGE.includes(usage.name)}
/>
</div> </div>
{/each} {/each}
</Layout>
</div> </div>
</Layout> </Layout>
<div /> </div>
{/if} {/if}
</Layout> </Layout>
</DashCard>
</Layout>
{/if} {/if}
<style> <style>
.usages { .usages {
display: grid; display: flex;
column-gap: 60px; flex-direction: column;
row-gap: 50px; }
grid-template-columns: 1fr 1fr 1fr; .detail :global(.spectrum-Detail) {
color: var(--spectrum-global-color-gray-700);
}
.detail :global(.icon) {
margin-bottom: 0;
} }
</style> </style>

View File

@ -1,14 +1,22 @@
import { writable } from "svelte/store" import { writable, get } from "svelte/store"
import { API } from "api" import { API } from "api"
import { auth } from "stores/portal"
import { Constants } from "@budibase/frontend-core"
import { StripeStatus } from "components/portal/licensing/constants"
export const createLicensingStore = () => { export const createLicensingStore = () => {
const DEFAULT = { const DEFAULT = {
plans: {}, plans: {},
} }
const oneDayInMilliseconds = 86400000
const store = writable(DEFAULT) const store = writable(DEFAULT)
const actions = { const actions = {
init: async () => {
await actions.getQuotaUsage()
await actions.getUsageMetrics()
},
getQuotaUsage: async () => { getQuotaUsage: async () => {
const quotaUsage = await API.getQuotaUsage() const quotaUsage = await API.getQuotaUsage()
store.update(state => { store.update(state => {
@ -18,6 +26,80 @@ export const createLicensingStore = () => {
} }
}) })
}, },
getUsageMetrics: async () => {
const quota = get(store).quotaUsage
const license = get(auth).user.license
const now = new Date()
const getMetrics = (keys, license, quota) => {
if (!license || !quota || !keys) {
return {}
}
return keys.reduce((acc, key) => {
const quotaLimit = license[key].value
const quotaUsed = (quota[key] / quotaLimit) * 100
acc[key] = quotaLimit > -1 ? Math.round(quotaUsed) : -1
return acc
}, {})
}
const monthlyMetrics = getMetrics(
["dayPasses", "queries", "automations"],
license.quotas.usage.monthly,
quota.monthly.current
)
const staticMetrics = getMetrics(
["apps", "rows"],
license.quotas.usage.static,
quota.usageQuota
)
const getDaysBetween = (dateStart, dateEnd) => {
return dateEnd > dateStart
? Math.round(
(dateEnd.getTime() - dateStart.getTime()) / oneDayInMilliseconds
)
: 0
}
const quotaResetDate = new Date(quota.quotaReset)
const quotaResetDaysRemaining = getDaysBetween(now, quotaResetDate)
const accountDowngraded =
license?.billing?.subscription?.downgradeAt &&
license?.billing?.subscription?.downgradeAt <= now.getTime() &&
license?.billing?.subscription?.status === StripeStatus.PAST_DUE &&
license?.plan.type === Constants.PlanType.FREE
const pastDueAtMilliseconds = license?.billing?.subscription?.pastDueAt
const downgradeAtMilliseconds =
license?.billing?.subscription?.downgradeAt
let pastDueDaysRemaining
let pastDueEndDate
if (pastDueAtMilliseconds && downgradeAtMilliseconds) {
pastDueEndDate = new Date(downgradeAtMilliseconds)
pastDueDaysRemaining = getDaysBetween(
new Date(pastDueAtMilliseconds),
pastDueEndDate
)
}
store.update(state => {
return {
...state,
usageMetrics: { ...monthlyMetrics, ...staticMetrics },
quotaResetDaysRemaining,
quotaResetDate,
accountDowngraded,
accountPastDue: pastDueAtMilliseconds != null,
pastDueEndDate,
pastDueDaysRemaining,
isFreePlan: () => {
return license?.plan.type === Constants.PlanType.FREE
},
}
})
},
} }
return { return {

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/cli", "name": "@budibase/cli",
"version": "1.3.15-alpha.7", "version": "1.3.19-alpha.6",
"description": "Budibase CLI, for developers, self hosting and migrations.", "description": "Budibase CLI, for developers, self hosting and migrations.",
"main": "src/index.js", "main": "src/index.js",
"bin": { "bin": {
@ -26,9 +26,9 @@
"outputPath": "build" "outputPath": "build"
}, },
"dependencies": { "dependencies": {
"@budibase/backend-core": "1.3.15-alpha.7", "@budibase/backend-core": "1.3.19-alpha.6",
"@budibase/string-templates": "1.3.15-alpha.7", "@budibase/string-templates": "1.3.19-alpha.6",
"@budibase/types": "1.3.15-alpha.7", "@budibase/types": "1.3.19-alpha.6",
"axios": "0.21.2", "axios": "0.21.2",
"chalk": "4.1.0", "chalk": "4.1.0",
"cli-progress": "3.11.2", "cli-progress": "3.11.2",

View File

@ -1,3 +1,5 @@
const { Event } = require("@budibase/types")
exports.CommandWords = { exports.CommandWords = {
BACKUPS: "backups", BACKUPS: "backups",
HOSTING: "hosting", HOSTING: "hosting",
@ -15,6 +17,7 @@ exports.AnalyticsEvents = {
OptOut: "analytics:opt:out", OptOut: "analytics:opt:out",
OptIn: "analytics:opt:in", OptIn: "analytics:opt:in",
SelfHostInit: "hosting:init", SelfHostInit: "hosting:init",
PluginInit: Event.PLUGIN_INIT,
} }
exports.POSTHOG_TOKEN = "phc_yGOn4i7jWKaCTapdGR6lfA4AvmuEQ2ijn5zAVSFYPlS" exports.POSTHOG_TOKEN = "phc_yGOn4i7jWKaCTapdGR6lfA4AvmuEQ2ijn5zAVSFYPlS"

View File

@ -0,0 +1,11 @@
const AnalyticsClient = require("./analytics/Client")
const client = new AnalyticsClient()
exports.captureEvent = (event, properties) => {
client.capture({
distinctId: "cli",
event,
properties,
})
}

View File

@ -13,7 +13,7 @@ const fs = require("fs")
const compose = require("docker-compose") const compose = require("docker-compose")
const makeEnv = require("./makeEnv") const makeEnv = require("./makeEnv")
const axios = require("axios") const axios = require("axios")
const AnalyticsClient = require("../analytics/Client") const { captureEvent } = require("../events")
const BUDIBASE_SERVICES = ["app-service", "worker-service", "proxy-service"] const BUDIBASE_SERVICES = ["app-service", "worker-service", "proxy-service"]
const ERROR_FILE = "docker-error.log" const ERROR_FILE = "docker-error.log"
@ -22,8 +22,6 @@ const FILE_URLS = [
] ]
const DO_USER_DATA_URL = "http://169.254.169.254/metadata/v1/user-data" const DO_USER_DATA_URL = "http://169.254.169.254/metadata/v1/user-data"
const client = new AnalyticsClient()
async function downloadFiles() { async function downloadFiles() {
const promises = [] const promises = []
for (let url of FILE_URLS) { for (let url of FILE_URLS) {
@ -72,12 +70,8 @@ async function init(type) {
return return
} }
} }
client.capture({ captureEvent(AnalyticsEvents.SelfHostInit, {
distinctId: "cli",
event: AnalyticsEvents.SelfHostInit,
properties: {
type, type,
},
}) })
await downloadFiles() await downloadFiles()
const config = isQuick ? makeEnv.QUICK_CONFIG : {} const config = isQuick ? makeEnv.QUICK_CONFIG : {}

View File

@ -1,5 +1,5 @@
const Command = require("../structures/Command") const Command = require("../structures/Command")
const { CommandWords } = require("../constants") const { CommandWords, AnalyticsEvents } = require("../constants")
const { getSkeleton, fleshOutSkeleton } = require("./skeleton") const { getSkeleton, fleshOutSkeleton } = require("./skeleton")
const questions = require("../questions") const questions = require("../questions")
const fs = require("fs") const fs = require("fs")
@ -8,6 +8,7 @@ const { validate } = require("@budibase/backend-core/plugins")
const { runPkgCommand } = require("../exec") const { runPkgCommand } = require("../exec")
const { join } = require("path") const { join } = require("path")
const { success, error, info, moveDirectory } = require("../utils") const { success, error, info, moveDirectory } = require("../utils")
const { captureEvent } = require("../events")
function checkInPlugin() { function checkInPlugin() {
if (!fs.existsSync("package.json")) { if (!fs.existsSync("package.json")) {
@ -58,7 +59,7 @@ async function init(opts) {
) )
return return
} }
const desc = await questions.string( const description = await questions.string(
"Description", "Description",
`An amazing Budibase ${type}!` `An amazing Budibase ${type}!`
) )
@ -67,7 +68,7 @@ async function init(opts) {
// get the skeleton // get the skeleton
console.log(info("Retrieving project...")) console.log(info("Retrieving project..."))
await getSkeleton(type, name) await getSkeleton(type, name)
await fleshOutSkeleton(type, name, desc, version) await fleshOutSkeleton(type, name, description, version)
console.log(info("Installing dependencies...")) console.log(info("Installing dependencies..."))
await runPkgCommand("install", join(process.cwd(), name)) await runPkgCommand("install", join(process.cwd(), name))
// if no parent directory desired move to cwd // if no parent directory desired move to cwd
@ -77,6 +78,12 @@ async function init(opts) {
} else { } else {
console.log(info(`Plugin created in directory "${name}"`)) console.log(info(`Plugin created in directory "${name}"`))
} }
captureEvent(AnalyticsEvents.PluginInit, {
type,
name,
description,
version,
})
} }
async function verify() { async function verify() {

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/client", "name": "@budibase/client",
"version": "1.3.15-alpha.7", "version": "1.3.19-alpha.6",
"license": "MPL-2.0", "license": "MPL-2.0",
"module": "dist/budibase-client.js", "module": "dist/budibase-client.js",
"main": "dist/budibase-client.js", "main": "dist/budibase-client.js",
@ -19,9 +19,9 @@
"dev:builder": "rollup -cw" "dev:builder": "rollup -cw"
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "1.3.15-alpha.7", "@budibase/bbui": "1.3.19-alpha.6",
"@budibase/frontend-core": "1.3.15-alpha.7", "@budibase/frontend-core": "1.3.19-alpha.6",
"@budibase/string-templates": "1.3.15-alpha.7", "@budibase/string-templates": "1.3.19-alpha.6",
"@spectrum-css/button": "^3.0.3", "@spectrum-css/button": "^3.0.3",
"@spectrum-css/card": "^3.0.3", "@spectrum-css/card": "^3.0.3",
"@spectrum-css/divider": "^1.0.3", "@spectrum-css/divider": "^1.0.3",

View File

@ -1,15 +1,20 @@
<a <script>
import { Link } from "@budibase/bbui"
</script>
<Link
href="https://www.budibase.com/?utm_source=budibase-apps-public-screens&utm_medium=badge&utm_campaign=made-in-budibase" href="https://www.budibase.com/?utm_source=budibase-apps-public-screens&utm_medium=badge&utm_campaign=made-in-budibase"
target="_blank"
> >
<div> <div>
<img src="https://i.imgur.com/Xhdt1YP.png" alt="Budibase" /> <img src="https://i.imgur.com/Xhdt1YP.png" alt="Budibase" />
<p>Made In Budibase</p> <p>Made with Budibase</p>
</div> </div>
</a> </Link>
<style> <style>
div { div {
position: absolute; position: fixed;
right: 20px; right: 20px;
bottom: 20px; bottom: 20px;
padding: 10px; padding: 10px;
@ -27,12 +32,7 @@
p { p {
text-decoration: none; text-decoration: none;
color: var(--spectrum-heading-m-text-color); color: var(--spectrum-global-color-gray-900);
}
a:visited {
text-decoration: none;
color: var(--spectrum-heading-m-text-color);
} }
img { img {

View File

@ -5,9 +5,18 @@
import { FieldTypes } from "constants" import { FieldTypes } from "constants"
import active from "svelte-spa-router/active" import active from "svelte-spa-router/active"
import { RoleUtils } from "@budibase/frontend-core" import { RoleUtils } from "@budibase/frontend-core"
import FreeLogo from "../FreeLogo.svelte"
import licensing from "../../licensing"
const sdk = getContext("sdk") const sdk = getContext("sdk")
const { routeStore, styleable, linkable, builderStore, currentRole } = sdk const {
routeStore,
styleable,
linkable,
builderStore,
currentRole,
environmentStore,
} = sdk
const component = getContext("component") const component = getContext("component")
const context = getContext("context") const context = getContext("context")
@ -225,6 +234,11 @@
</div> </div>
</div> </div>
{/if} {/if}
{#if !$builderStore.inBuilder && licensing.logoEnabled() && $environmentStore.cloud}
<FreeLogo />
{/if}
<div class="main-wrapper"> <div class="main-wrapper">
<div class="main size--{pageWidthClass}"> <div class="main size--{pageWidthClass}">
<slot /> <slot />

View File

@ -0,0 +1,6 @@
export const PlanType = {
FREE: "free",
TEAM: "team",
BUSINESS: "business",
ENTERPRISE: "enterprise",
}

View File

@ -0,0 +1,5 @@
import { isFreePlan } from "./utils.js"
export const logoEnabled = () => {
return isFreePlan()
}

View File

@ -0,0 +1,7 @@
import * as features from "./features"
const licensing = {
...features,
}
export default licensing

View File

@ -0,0 +1,20 @@
import { authStore } from "../stores/auth.js"
import { get } from "svelte/store"
import { PlanType } from "./constants"
const getLicense = () => {
const user = get(authStore)
if (user) {
return user.license
}
}
export const isFreePlan = () => {
const license = getLicense()
if (license) {
return license.plan.type === PlanType.FREE
} else {
// safety net - no license means free plan
return true
}
}

View File

@ -9,6 +9,7 @@ import {
rowSelectionStore, rowSelectionStore,
componentStore, componentStore,
currentRole, currentRole,
environmentStore,
} from "stores" } from "stores"
import { styleable } from "utils/styleable" import { styleable } from "utils/styleable"
import { linkable } from "utils/linkable" import { linkable } from "utils/linkable"
@ -27,6 +28,7 @@ export default {
builderStore, builderStore,
uploadStore, uploadStore,
componentStore, componentStore,
environmentStore,
currentRole, currentRole,
styleable, styleable,
linkable, linkable,

View File

@ -0,0 +1,31 @@
import { API } from "api"
import { writable } from "svelte/store"
const initialState = {
cloud: false,
}
const createEnvironmentStore = () => {
const store = writable(initialState)
const actions = {
fetchEnvironment: async () => {
try {
const environment = await API.getEnvironment()
store.set({
...initialState,
...environment,
})
} catch (error) {
store.set(initialState)
}
},
}
return {
subscribe: store.subscribe,
actions,
}
}
export const environmentStore = createEnvironmentStore()

View File

@ -17,6 +17,7 @@ export { devToolsStore } from "./devTools"
export { componentStore } from "./components" export { componentStore } from "./components"
export { uploadStore } from "./uploads.js" export { uploadStore } from "./uploads.js"
export { rowSelectionStore } from "./rowSelection.js" export { rowSelectionStore } from "./rowSelection.js"
export { environmentStore } from "./environment"
// Context stores are layered and duplicated, so it is not a singleton // Context stores are layered and duplicated, so it is not a singleton
export { createContextStore } from "./context" export { createContextStore } from "./context"

View File

@ -1,7 +1,9 @@
import { routeStore } from "./routes" import { routeStore } from "./routes"
import { appStore } from "./app" import { appStore } from "./app"
import { environmentStore } from "./environment"
export async function initialise() { export async function initialise() {
await routeStore.actions.fetchRoutes() await routeStore.actions.fetchRoutes()
await appStore.actions.fetchAppDefinition() await appStore.actions.fetchAppDefinition()
await environmentStore.actions.fetchEnvironment()
} }

View File

@ -1,12 +1,12 @@
{ {
"name": "@budibase/frontend-core", "name": "@budibase/frontend-core",
"version": "1.3.15-alpha.7", "version": "1.3.19-alpha.6",
"description": "Budibase frontend core libraries used in builder and client", "description": "Budibase frontend core libraries used in builder and client",
"author": "Budibase", "author": "Budibase",
"license": "MPL-2.0", "license": "MPL-2.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"dependencies": { "dependencies": {
"@budibase/bbui": "1.3.15-alpha.7", "@budibase/bbui": "1.3.19-alpha.6",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"svelte": "^3.46.2" "svelte": "^3.46.2"
} }

View File

@ -57,6 +57,7 @@ export const Cookies = {
Auth: "budibase:auth", Auth: "budibase:auth",
CurrentApp: "budibase:currentapp", CurrentApp: "budibase:currentapp",
ReturnUrl: "budibase:returnurl", ReturnUrl: "budibase:returnurl",
AccountReturnUrl: "budibase:account:returnurl",
} }
// Table names // Table names

View File

@ -121,7 +121,7 @@ export const buildLuceneQuery = filter => {
query.allOr = true query.allOr = true
return return
} }
if (type === "datetime") { if (type === "datetime" && !isHbs) {
// Ensure date value is a valid date and parse into correct format // Ensure date value is a valid date and parse into correct format
if (!value) { if (!value) {
return return

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/server", "name": "@budibase/server",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "1.3.15-alpha.7", "version": "1.3.19-alpha.6",
"description": "Budibase Web Server", "description": "Budibase Web Server",
"main": "src/index.ts", "main": "src/index.ts",
"repository": { "repository": {
@ -77,11 +77,11 @@
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@apidevtools/swagger-parser": "10.0.3", "@apidevtools/swagger-parser": "10.0.3",
"@budibase/backend-core": "1.3.15-alpha.7", "@budibase/backend-core": "1.3.19-alpha.6",
"@budibase/client": "1.3.15-alpha.7", "@budibase/client": "1.3.19-alpha.6",
"@budibase/pro": "1.3.15-alpha.7", "@budibase/pro": "1.3.19-alpha.6",
"@budibase/string-templates": "1.3.15-alpha.7", "@budibase/string-templates": "1.3.19-alpha.6",
"@budibase/types": "1.3.15-alpha.7", "@budibase/types": "1.3.19-alpha.6",
"@bull-board/api": "3.7.0", "@bull-board/api": "3.7.0",
"@bull-board/koa": "3.9.4", "@bull-board/koa": "3.9.4",
"@elastic/elasticsearch": "7.10.0", "@elastic/elasticsearch": "7.10.0",

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