Merge branch 'develop' of github.com:Budibase/budibase into public-api-sdk
This commit is contained in:
commit
8a219fa0e6
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -65,7 +65,7 @@ Budibase is open-source - licensed as GPL v3. This should fill you with confiden
|
||||||
<br /><br />
|
<br /><br />
|
||||||
|
|
||||||
### Load data or start from scratch
|
### Load data or start from scratch
|
||||||
Budibase pulls in data from multiple sources, including MongoDB, CouchDB, PostgreSQL, MySQL, Airtable, S3, DynamoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no data sources. [Request new data sources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
Budibase pulls in data from multiple sources, including MongoDB, CouchDB, PostgreSQL, MySQL, Airtable, S3, DynamoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no datasources. [Request new datasources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img alt="Budibase data" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970242/Out%20of%20beta%20launch/data_n1tlhf.png">
|
<img alt="Budibase data" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970242/Out%20of%20beta%20launch/data_n1tlhf.png">
|
||||||
|
|
|
@ -348,7 +348,7 @@ export interface paths {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
responses: {
|
responses: {
|
||||||
/** Returns the created table, including the ID which has been generated for it. This can be internal or external data sources. */
|
/** Returns the created table, including the ID which has been generated for it. This can be internal or external datasources. */
|
||||||
200: {
|
200: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["tableOutput"]
|
"application/json": components["schemas"]["tableOutput"]
|
||||||
|
@ -959,7 +959,7 @@ export interface components {
|
||||||
query: {
|
query: {
|
||||||
/** @description The ID of the query. */
|
/** @description The ID of the query. */
|
||||||
_id: string
|
_id: string
|
||||||
/** @description The ID of the data source the query belongs to. */
|
/** @description The ID of the datasource the query belongs to. */
|
||||||
datasourceId?: string
|
datasourceId?: string
|
||||||
/** @description The bindings which are required to perform this query. */
|
/** @description The bindings which are required to perform this query. */
|
||||||
parameters?: string[]
|
parameters?: string[]
|
||||||
|
@ -983,7 +983,7 @@ export interface components {
|
||||||
data: {
|
data: {
|
||||||
/** @description The ID of the query. */
|
/** @description The ID of the query. */
|
||||||
_id: string
|
_id: string
|
||||||
/** @description The ID of the data source the query belongs to. */
|
/** @description The ID of the datasource the query belongs to. */
|
||||||
datasourceId?: string
|
datasourceId?: string
|
||||||
/** @description The bindings which are required to perform this query. */
|
/** @description The bindings which are required to perform this query. */
|
||||||
parameters?: string[]
|
parameters?: string[]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
ssh-keygen -A
|
||||||
|
|
||||||
|
#prepare run dir
|
||||||
|
if [ ! -d "/var/run/sshd" ]; then
|
||||||
|
mkdir -p /var/run/sshd
|
||||||
|
fi
|
|
@ -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
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "1.3.15-alpha.7",
|
"version": "1.3.19-alpha.6",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
class BudibaseError extends Error {
|
|
||||||
constructor(message, code, type) {
|
|
||||||
super(message)
|
|
||||||
this.code = code
|
|
||||||
this.type = type
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
BudibaseError,
|
|
||||||
}
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
export class BudibaseError extends Error {
|
||||||
|
code: string
|
||||||
|
type: string
|
||||||
|
|
||||||
|
constructor(message: string, code: string, type: string) {
|
||||||
|
super(message)
|
||||||
|
this.code = code
|
||||||
|
this.type = type
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +0,0 @@
|
||||||
const { BudibaseError } = require("./base")
|
|
||||||
|
|
||||||
class GenericError extends BudibaseError {
|
|
||||||
constructor(message, code, type) {
|
|
||||||
super(message, code, type ? type : "generic")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
GenericError,
|
|
||||||
}
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { BudibaseError } from "./base"
|
||||||
|
|
||||||
|
export class GenericError extends BudibaseError {
|
||||||
|
constructor(message: string, code: string, type: string) {
|
||||||
|
super(message, code, type ? type : "generic")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +0,0 @@
|
||||||
const { GenericError } = require("./generic")
|
|
||||||
|
|
||||||
class HTTPError extends GenericError {
|
|
||||||
constructor(message, httpStatus, code = "http", type = "generic") {
|
|
||||||
super(message, code, type)
|
|
||||||
this.status = httpStatus
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
HTTPError,
|
|
||||||
}
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { GenericError } from "./generic"
|
||||||
|
|
||||||
|
export class HTTPError extends GenericError {
|
||||||
|
status: number
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
httpStatus: number,
|
||||||
|
code = "http",
|
||||||
|
type = "generic"
|
||||||
|
) {
|
||||||
|
super(message, code, type)
|
||||||
|
this.status = httpStatus
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
const http = require("./http")
|
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
|
|
@ -1,43 +0,0 @@
|
||||||
const { HTTPError } = require("./http")
|
|
||||||
|
|
||||||
const type = "license_error"
|
|
||||||
|
|
||||||
const codes = {
|
|
||||||
USAGE_LIMIT_EXCEEDED: "usage_limit_exceeded",
|
|
||||||
FEATURE_DISABLED: "feature_disabled",
|
|
||||||
}
|
|
||||||
|
|
||||||
const context = {
|
|
||||||
[codes.USAGE_LIMIT_EXCEEDED]: err => {
|
|
||||||
return {
|
|
||||||
limitName: err.limitName,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[codes.FEATURE_DISABLED]: err => {
|
|
||||||
return {
|
|
||||||
featureName: err.featureName,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
class UsageLimitError extends HTTPError {
|
|
||||||
constructor(message, limitName) {
|
|
||||||
super(message, 400, codes.USAGE_LIMIT_EXCEEDED, type)
|
|
||||||
this.limitName = limitName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FeatureDisabledError extends HTTPError {
|
|
||||||
constructor(message, featureName) {
|
|
||||||
super(message, 400, codes.FEATURE_DISABLED, type)
|
|
||||||
this.featureName = featureName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
type,
|
|
||||||
codes,
|
|
||||||
context,
|
|
||||||
UsageLimitError,
|
|
||||||
FeatureDisabledError,
|
|
||||||
}
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { HTTPError } from "./http"
|
||||||
|
|
||||||
|
export const type = "license_error"
|
||||||
|
|
||||||
|
export const codes = {
|
||||||
|
USAGE_LIMIT_EXCEEDED: "usage_limit_exceeded",
|
||||||
|
FEATURE_DISABLED: "feature_disabled",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const context = {
|
||||||
|
[codes.USAGE_LIMIT_EXCEEDED]: (err: any) => {
|
||||||
|
return {
|
||||||
|
limitName: err.limitName,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[codes.FEATURE_DISABLED]: (err: any) => {
|
||||||
|
return {
|
||||||
|
featureName: err.featureName,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UsageLimitError extends HTTPError {
|
||||||
|
limitName: string
|
||||||
|
|
||||||
|
constructor(message: string, limitName: string) {
|
||||||
|
super(message, 400, codes.USAGE_LIMIT_EXCEEDED, type)
|
||||||
|
this.limitName = limitName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FeatureDisabledError extends HTTPError {
|
||||||
|
featureName: string
|
||||||
|
|
||||||
|
constructor(message: string, featureName: string) {
|
||||||
|
super(message, 400, codes.FEATURE_DISABLED, type)
|
||||||
|
this.featureName = featureName
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,8 +5,15 @@ import {
|
||||||
DatasourceCreatedEvent,
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { publishEvent } from "../events"
|
||||||
|
import {
|
||||||
|
Event,
|
||||||
|
Plugin,
|
||||||
|
PluginDeletedEvent,
|
||||||
|
PluginImportedEvent,
|
||||||
|
PluginInitEvent,
|
||||||
|
} from "@budibase/types"
|
||||||
|
|
||||||
|
export async function init(plugin: Plugin) {
|
||||||
|
const properties: PluginInitEvent = {
|
||||||
|
type: plugin.schema.type,
|
||||||
|
name: plugin.name,
|
||||||
|
description: plugin.description,
|
||||||
|
version: plugin.version,
|
||||||
|
}
|
||||||
|
await publishEvent(Event.PLUGIN_INIT, properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function imported(plugin: Plugin) {
|
||||||
|
const properties: PluginImportedEvent = {
|
||||||
|
pluginId: plugin._id as string,
|
||||||
|
type: plugin.schema.type,
|
||||||
|
source: plugin.source,
|
||||||
|
name: plugin.name,
|
||||||
|
description: plugin.description,
|
||||||
|
version: plugin.version,
|
||||||
|
}
|
||||||
|
await publishEvent(Event.PLUGIN_IMPORTED, properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleted(plugin: Plugin) {
|
||||||
|
const properties: PluginDeletedEvent = {
|
||||||
|
pluginId: plugin._id as string,
|
||||||
|
type: plugin.schema.type,
|
||||||
|
name: plugin.name,
|
||||||
|
description: plugin.description,
|
||||||
|
version: plugin.version,
|
||||||
|
}
|
||||||
|
await publishEvent(Event.PLUGIN_DELETED, properties)
|
||||||
|
}
|
|
@ -31,20 +31,26 @@ const TENANT_FEATURE_FLAGS = getFeatureFlags()
|
||||||
|
|
||||||
exports.isEnabled = featureFlag => {
|
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 = {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
}
|
|
@ -1,29 +1,39 @@
|
||||||
const {
|
import {
|
||||||
ViewName,
|
ViewName,
|
||||||
getUsersByAppParams,
|
getUsersByAppParams,
|
||||||
getProdAppID,
|
getProdAppID,
|
||||||
generateAppUserID,
|
generateAppUserID,
|
||||||
} = require("./db/utils")
|
} from "./db/utils"
|
||||||
const { queryGlobalView } = require("./db/views")
|
import { queryGlobalView } from "./db/views"
|
||||||
const { UNICODE_MAX } = require("./db/constants")
|
import { UNICODE_MAX } from "./db/constants"
|
||||||
|
import { User } from "@budibase/types"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given an email address this will use a view to search through
|
* Given an email address this will use a view to search through
|
||||||
* all the users to find one with this email address.
|
* all the users to find one with this email address.
|
||||||
* @param {string} email the email to lookup the user by.
|
* @param {string} email the email to lookup the user by.
|
||||||
*/
|
*/
|
||||||
exports.getGlobalUserByEmail = async email => {
|
export const getGlobalUserByEmail = async (
|
||||||
|
email: String
|
||||||
|
): Promise<User | undefined> => {
|
||||||
if (email == null) {
|
if (email == null) {
|
||||||
throw "Must supply an email address to view"
|
throw "Must supply an email address to view"
|
||||||
}
|
}
|
||||||
|
|
||||||
return await queryGlobalView(ViewName.USER_BY_EMAIL, {
|
const response = await queryGlobalView<User>(ViewName.USER_BY_EMAIL, {
|
||||||
key: email.toLowerCase(),
|
key: email.toLowerCase(),
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (Array.isArray(response)) {
|
||||||
|
// shouldn't be able to happen, but need to handle just in case
|
||||||
|
throw new Error(`Multiple users found with email address: ${email}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.searchGlobalUsersByApp = async (appId, opts) => {
|
export const searchGlobalUsersByApp = async (appId: any, opts: any) => {
|
||||||
if (typeof appId !== "string") {
|
if (typeof appId !== "string") {
|
||||||
throw new Error("Must provide a string based app ID")
|
throw new Error("Must provide a string based app ID")
|
||||||
}
|
}
|
||||||
|
@ -38,24 +48,24 @@ exports.searchGlobalUsersByApp = async (appId, opts) => {
|
||||||
return Array.isArray(response) ? response : [response]
|
return Array.isArray(response) ? response : [response]
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getGlobalUserByAppPage = (appId, user) => {
|
export const getGlobalUserByAppPage = (appId: string, user: User) => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return generateAppUserID(getProdAppID(appId), user._id)
|
return generateAppUserID(getProdAppID(appId), user._id!)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performs a starts with search on the global email view.
|
* Performs a starts with search on the global email view.
|
||||||
*/
|
*/
|
||||||
exports.searchGlobalUsersByEmail = async (email, opts) => {
|
export const searchGlobalUsersByEmail = async (email: string, opts: any) => {
|
||||||
if (typeof email !== "string") {
|
if (typeof email !== "string") {
|
||||||
throw new Error("Must provide a string to search by")
|
throw new Error("Must provide a string to search by")
|
||||||
}
|
}
|
||||||
const lcEmail = email.toLowerCase()
|
const lcEmail = email.toLowerCase()
|
||||||
// handle if passing up startkey for pagination
|
// handle if passing up startkey for pagination
|
||||||
const startkey = opts && opts.startkey ? opts.startkey : lcEmail
|
const startkey = opts && opts.startkey ? opts.startkey : lcEmail
|
||||||
let response = await queryGlobalView(ViewName.USER_BY_EMAIL, {
|
let response = await queryGlobalView<User>(ViewName.USER_BY_EMAIL, {
|
||||||
...opts,
|
...opts,
|
||||||
startkey,
|
startkey,
|
||||||
endkey: `${lcEmail}${UNICODE_MAX}`,
|
endkey: `${lcEmail}${UNICODE_MAX}`,
|
|
@ -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.
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -82,10 +82,10 @@ filterTests(['smoke', 'all'], () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
if (Cypress.env("TEST_ENV")) {
|
if (Cypress.env("TEST_ENV")) {
|
||||||
it("should generate data source screens", () => {
|
it("should generate datasource screens", () => {
|
||||||
// Using MySQL data source for testing this
|
// Using MySQL datasource for testing this
|
||||||
const datasource = "MySQL"
|
const datasource = "MySQL"
|
||||||
// Select & configure MySQL data source
|
// Select & configure MySQL datasource
|
||||||
cy.selectExternalDatasource(datasource)
|
cy.selectExternalDatasource(datasource)
|
||||||
cy.addDatasourceConfig(datasource)
|
cy.addDatasourceConfig(datasource)
|
||||||
// Create Autogenerated screens from a MySQL table - MySQL contains books table
|
// Create Autogenerated screens from a MySQL table - MySQL contains books table
|
||||||
|
|
|
@ -11,8 +11,8 @@ filterTests(["all"], () => {
|
||||||
const queryName = "Cypress Test Query"
|
const queryName = "Cypress Test Query"
|
||||||
const queryRename = "CT Query Rename"
|
const queryRename = "CT Query Rename"
|
||||||
|
|
||||||
it("Should add MySQL data source without configuration", () => {
|
it("Should add MySQL datasource without configuration", () => {
|
||||||
// Select MySQL data source
|
// Select MySQL datasource
|
||||||
cy.selectExternalDatasource(datasource)
|
cy.selectExternalDatasource(datasource)
|
||||||
// Attempt to fetch tables without applying configuration
|
// Attempt to fetch tables without applying configuration
|
||||||
cy.intercept("**/datasources").as("datasource")
|
cy.intercept("**/datasources").as("datasource")
|
||||||
|
@ -35,8 +35,8 @@ filterTests(["all"], () => {
|
||||||
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true })
|
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should add MySQL data source and fetch tables", () => {
|
it("should add MySQL datasource and fetch tables", () => {
|
||||||
// Add & configure MySQL data source
|
// Add & configure MySQL datasource
|
||||||
cy.selectExternalDatasource(datasource)
|
cy.selectExternalDatasource(datasource)
|
||||||
cy.intercept("**/datasources").as("datasource")
|
cy.intercept("**/datasources").as("datasource")
|
||||||
cy.addDatasourceConfig(datasource)
|
cy.addDatasourceConfig(datasource)
|
||||||
|
@ -52,7 +52,7 @@ filterTests(["all"], () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should check table fetching error", () => {
|
it("should check table fetching error", () => {
|
||||||
// MySQL test data source contains tables without primary keys
|
// MySQL test datasource contains tables without primary keys
|
||||||
cy.get(".spectrum-InLineAlert")
|
cy.get(".spectrum-InLineAlert")
|
||||||
.should("contain", "Error fetching tables")
|
.should("contain", "Error fetching tables")
|
||||||
.and("contain", "No primary key constraint found")
|
.and("contain", "No primary key constraint found")
|
||||||
|
|
|
@ -11,8 +11,8 @@ filterTests(["all"], () => {
|
||||||
const queryName = "Cypress Test Query"
|
const queryName = "Cypress Test Query"
|
||||||
const queryRename = "CT Query Rename"
|
const queryRename = "CT Query Rename"
|
||||||
|
|
||||||
it("Should add Oracle data source and skip table fetch", () => {
|
it("Should add Oracle datasource and skip table fetch", () => {
|
||||||
// Select Oracle data source
|
// Select Oracle datasource
|
||||||
cy.selectExternalDatasource(datasource)
|
cy.selectExternalDatasource(datasource)
|
||||||
// Skip table fetch - no config added
|
// Skip table fetch - no config added
|
||||||
cy.get(".spectrum-Button")
|
cy.get(".spectrum-Button")
|
||||||
|
@ -23,7 +23,7 @@ filterTests(["all"], () => {
|
||||||
cy.get(".spectrum-Textfield-input", { timeout: 500 })
|
cy.get(".spectrum-Textfield-input", { timeout: 500 })
|
||||||
.eq(1)
|
.eq(1)
|
||||||
.should("have.value", "localhost")
|
.should("have.value", "localhost")
|
||||||
// Add another Oracle data source, configure & skip table fetch
|
// Add another Oracle datasource, configure & skip table fetch
|
||||||
cy.selectExternalDatasource(datasource)
|
cy.selectExternalDatasource(datasource)
|
||||||
cy.addDatasourceConfig(datasource, true)
|
cy.addDatasourceConfig(datasource, true)
|
||||||
// Confirm config and no tables
|
// Confirm config and no tables
|
||||||
|
@ -33,8 +33,8 @@ filterTests(["all"], () => {
|
||||||
cy.get(".spectrum-Body").eq(2).should("contain", "No tables found.")
|
cy.get(".spectrum-Body").eq(2).should("contain", "No tables found.")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Should add Oracle data source and fetch tables without configuration", () => {
|
it("Should add Oracle datasource and fetch tables without configuration", () => {
|
||||||
// Select Oracle data source
|
// Select Oracle datasource
|
||||||
cy.selectExternalDatasource(datasource)
|
cy.selectExternalDatasource(datasource)
|
||||||
// Attempt to fetch tables without applying configuration
|
// Attempt to fetch tables without applying configuration
|
||||||
cy.intercept("**/datasources").as("datasource")
|
cy.intercept("**/datasources").as("datasource")
|
||||||
|
@ -49,8 +49,8 @@ filterTests(["all"], () => {
|
||||||
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true })
|
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
xit("should add Oracle data source and fetch tables", () => {
|
xit("should add Oracle datasource and fetch tables", () => {
|
||||||
// Add & configure Oracle data source
|
// Add & configure Oracle datasource
|
||||||
cy.selectExternalDatasource(datasource)
|
cy.selectExternalDatasource(datasource)
|
||||||
cy.intercept("**/datasources").as("datasource")
|
cy.intercept("**/datasources").as("datasource")
|
||||||
cy.addDatasourceConfig(datasource)
|
cy.addDatasourceConfig(datasource)
|
||||||
|
|
|
@ -11,8 +11,8 @@ filterTests(["all"], () => {
|
||||||
const queryName = "Cypress Test Query"
|
const queryName = "Cypress Test Query"
|
||||||
const queryRename = "CT Query Rename"
|
const queryRename = "CT Query Rename"
|
||||||
|
|
||||||
xit("Should add PostgreSQL data source without configuration", () => {
|
xit("Should add PostgreSQL datasource without configuration", () => {
|
||||||
// Select PostgreSQL data source
|
// Select PostgreSQL datasource
|
||||||
cy.selectExternalDatasource(datasource)
|
cy.selectExternalDatasource(datasource)
|
||||||
// Attempt to fetch tables without applying configuration
|
// Attempt to fetch tables without applying configuration
|
||||||
cy.intercept("**/datasources").as("datasource")
|
cy.intercept("**/datasources").as("datasource")
|
||||||
|
@ -27,8 +27,8 @@ filterTests(["all"], () => {
|
||||||
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true })
|
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should add PostgreSQL data source and fetch tables", () => {
|
it("should add PostgreSQL datasource and fetch tables", () => {
|
||||||
// Add & configure PostgreSQL data source
|
// Add & configure PostgreSQL datasource
|
||||||
cy.selectExternalDatasource(datasource)
|
cy.selectExternalDatasource(datasource)
|
||||||
cy.intercept("**/datasources").as("datasource")
|
cy.intercept("**/datasources").as("datasource")
|
||||||
cy.addDatasourceConfig(datasource)
|
cy.addDatasourceConfig(datasource)
|
||||||
|
|
|
@ -10,8 +10,8 @@ filterTests(["smoke", "all"], () => {
|
||||||
const datasource = "REST"
|
const datasource = "REST"
|
||||||
const restUrl = "https://api.openbrewerydb.org/breweries"
|
const restUrl = "https://api.openbrewerydb.org/breweries"
|
||||||
|
|
||||||
it("Should add REST data source with incorrect API", () => {
|
it("Should add REST datasource with incorrect API", () => {
|
||||||
// Select REST data source
|
// Select REST datasource
|
||||||
cy.selectExternalDatasource(datasource)
|
cy.selectExternalDatasource(datasource)
|
||||||
// Enter incorrect api & attempt to send query
|
// Enter incorrect api & attempt to send query
|
||||||
cy.get(".query-buttons", { timeout: 1000 }).contains("Add query").click({ force: true })
|
cy.get(".query-buttons", { timeout: 1000 }).contains("Add query").click({ force: true })
|
||||||
|
|
|
@ -763,7 +763,7 @@ Cypress.Commands.add("navigateToDataSection", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("navigateToAutogeneratedModal", () => {
|
Cypress.Commands.add("navigateToAutogeneratedModal", () => {
|
||||||
// Screen name must already exist within data source
|
// Screen name must already exist within datasource
|
||||||
cy.contains("Design").click()
|
cy.contains("Design").click()
|
||||||
cy.get(".spectrum-Button").contains("Add screen").click({ force: true })
|
cy.get(".spectrum-Button").contains("Add screen").click({ force: true })
|
||||||
cy.get(".spectrum-Modal").within(() => {
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
|
@ -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()
|
||||||
})
|
})
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -27,7 +27,7 @@ export async function saveDatasource(config, skipFetch = false) {
|
||||||
// Create datasource
|
// Create datasource
|
||||||
const resp = await datasources.save(datasource, !skipFetch && datasource.plus)
|
const resp = await datasources.save(datasource, !skipFetch && datasource.plus)
|
||||||
|
|
||||||
// update the tables incase data source plus
|
// update the tables incase datasource plus
|
||||||
await tables.fetch()
|
await tables.fetch()
|
||||||
await datasources.select(resp._id)
|
await datasources.select(resp._id)
|
||||||
return resp
|
return resp
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 },
|
||||||
|
}
|
||||||
|
}
|
|
@ -50,7 +50,6 @@
|
||||||
type="string"
|
type="string"
|
||||||
{bindings}
|
{bindings}
|
||||||
fillWidth={true}
|
fillWidth={true}
|
||||||
allowJS={false}
|
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -32,8 +32,8 @@
|
||||||
: []
|
: []
|
||||||
$: openDataSource = enrichedDataSources.find(x => x.open)
|
$: openDataSource = enrichedDataSources.find(x => x.open)
|
||||||
$: {
|
$: {
|
||||||
// Ensure the open data source is always included in the list of open
|
// Ensure the open datasource is always included in the list of open
|
||||||
// data sources
|
// datasources
|
||||||
if (openDataSource) {
|
if (openDataSource) {
|
||||||
openNode(openDataSource)
|
openNode(openDataSource)
|
||||||
}
|
}
|
||||||
|
@ -79,7 +79,7 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
const containsActiveEntity = datasource => {
|
const containsActiveEntity = datasource => {
|
||||||
// If we're view a query then the data source ID is in the URL
|
// If we're view a query then the datasource ID is in the URL
|
||||||
if ($params.selectedDatasource === datasource._id) {
|
if ($params.selectedDatasource === datasource._id) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -126,7 +126,7 @@
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
disabled={!Object.keys(integration).length}
|
disabled={!Object.keys(integration).length}
|
||||||
title="Add data source"
|
title="Add datasource"
|
||||||
confirmText="Continue"
|
confirmText="Continue"
|
||||||
showSecondaryButton={showImportButton}
|
showSecondaryButton={showImportButton}
|
||||||
secondaryButtonText="Import"
|
secondaryButtonText="Import"
|
||||||
|
@ -155,7 +155,7 @@
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<Layout noPadding gap="XS">
|
<Layout noPadding gap="XS">
|
||||||
<Body size="S">Connect to an external data source</Body>
|
<Body size="S">Connect to an external datasource</Body>
|
||||||
<div class="item-list">
|
<div class="item-list">
|
||||||
{#each Object.entries(integrations).filter(([key, val]) => key !== IntegrationTypes.INTERNAL && !val.custom) as [integrationType, schema]}
|
{#each Object.entries(integrations).filter(([key, val]) => key !== IntegrationTypes.INTERNAL && !val.custom) as [integrationType, schema]}
|
||||||
<DatasourceCard
|
<DatasourceCard
|
||||||
|
@ -170,7 +170,7 @@
|
||||||
|
|
||||||
{#if customIntegrations.length > 0}
|
{#if customIntegrations.length > 0}
|
||||||
<Layout noPadding gap="XS">
|
<Layout noPadding gap="XS">
|
||||||
<Body size="S">Custom data source</Body>
|
<Body size="S">Custom datasource</Body>
|
||||||
<div class="item-list">
|
<div class="item-list">
|
||||||
{#each customIntegrations as [integrationType, schema]}
|
{#each customIntegrations as [integrationType, schema]}
|
||||||
<DatasourceCard
|
<DatasourceCard
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import TemplateCard from "components/common/TemplateCard.svelte"
|
import TemplateCard from "components/common/TemplateCard.svelte"
|
||||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||||
|
import { licensing } from "stores/portal"
|
||||||
|
|
||||||
export let templates
|
export let templates
|
||||||
|
|
||||||
|
@ -96,15 +97,17 @@
|
||||||
backgroundColour={templateEntry.background}
|
backgroundColour={templateEntry.background}
|
||||||
icon={templateEntry.icon}
|
icon={templateEntry.icon}
|
||||||
>
|
>
|
||||||
<Button
|
{#if $licensing?.usageMetrics?.apps < 100}
|
||||||
cta
|
<Button
|
||||||
on:click={() => {
|
cta
|
||||||
template = templateEntry
|
on:click={() => {
|
||||||
creationModal.show()
|
template = templateEntry
|
||||||
}}
|
creationModal.show()
|
||||||
>
|
}}
|
||||||
Use template
|
>
|
||||||
</Button>
|
Use template
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
<a
|
<a
|
||||||
href={templateEntry.url}
|
href={templateEntry.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|
|
@ -73,13 +73,13 @@
|
||||||
|
|
||||||
<div class="root">
|
<div class="root">
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
Choose the data source that provides the row you would like to duplicate.
|
Choose the datasource that provides the row you would like to duplicate.
|
||||||
<br />
|
<br />
|
||||||
You can always add or override fields manually.
|
You can always add or override fields manually.
|
||||||
</Body>
|
</Body>
|
||||||
|
|
||||||
<div class="params">
|
<div class="params">
|
||||||
<Label small>Data Source</Label>
|
<Label small>Datasource</Label>
|
||||||
<Select
|
<Select
|
||||||
bind:value={parameters.providerId}
|
bind:value={parameters.providerId}
|
||||||
options={providerOptions}
|
options={providerOptions}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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}
|
||||||
|
/>
|
|
@ -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>
|
|
@ -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",
|
||||||
|
}
|
|
@ -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()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as Usage } from "./Usage.svelte"
|
||||||
|
export { default as DashCard } from "./UsageDashCard.svelte"
|
|
@ -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",
|
||||||
|
|
|
@ -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"] })
|
||||||
|
|
|
@ -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 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -77,7 +77,7 @@
|
||||||
size="L"
|
size="L"
|
||||||
>
|
>
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
Select which data source you would like to use to create your screens
|
Select which datasource you would like to use to create your screens
|
||||||
</Body>
|
</Body>
|
||||||
<Layout noPadding gap="S">
|
<Layout noPadding gap="S">
|
||||||
{#each filteredSources as datasource}
|
{#each filteredSources as datasource}
|
||||||
|
|
|
@ -66,7 +66,7 @@
|
||||||
<Heading size="XS">Autogenerated screens</Heading>
|
<Heading size="XS">Autogenerated screens</Heading>
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
Add autogenerated screens with CRUD functionality to get a working
|
Add autogenerated screens with CRUD functionality to get a working
|
||||||
app quickly! (Requires a data source)
|
app quickly! (Requires a datasource)
|
||||||
</Body>
|
</Body>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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,9 +45,13 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
const initiateAppCreation = () => {
|
const initiateAppCreation = () => {
|
||||||
template = null
|
if ($licensing.usageMetrics.apps >= 100) {
|
||||||
creationModal.show()
|
appLimitModal.show()
|
||||||
creatingApp = true
|
} else {
|
||||||
|
template = null
|
||||||
|
creationModal.show()
|
||||||
|
creatingApp = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const stopAppCreation = () => {
|
const stopAppCreation = () => {
|
||||||
|
@ -52,9 +60,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const initiateAppImport = () => {
|
const initiateAppImport = () => {
|
||||||
template = { fromFile: true }
|
if ($licensing.usageMetrics.apps >= 100) {
|
||||||
creationModal.show()
|
appLimitModal.show()
|
||||||
creatingApp = true
|
} else {
|
||||||
|
template = { fromFile: true }
|
||||||
|
creationModal.show()
|
||||||
|
creatingApp = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -121,6 +133,7 @@
|
||||||
>
|
>
|
||||||
<CreateAppModal {template} />
|
<CreateAppModal {template} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
<AppLimitModal bind:this={appLimitModal} />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.title .welcome > .buttons {
|
.title .welcome > .buttons {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -38,7 +38,11 @@
|
||||||
try {
|
try {
|
||||||
await groups.actions.save(group)
|
await groups.actions.save(group)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error(`Failed to save group`)
|
if (error.status === 400) {
|
||||||
|
notifications.error(error.message)
|
||||||
|
} else {
|
||||||
|
notifications.error(`Failed to save group`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -35,10 +35,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const activate = async () => {
|
const activate = async () => {
|
||||||
await API.activateLicenseKey({ licenseKey })
|
try {
|
||||||
await auth.getSelf()
|
await API.activateLicenseKey({ licenseKey })
|
||||||
await setLicenseInfo()
|
await auth.getSelf()
|
||||||
notifications.success("Successfully activated")
|
await setLicenseInfo()
|
||||||
|
notifications.success("Successfully activated")
|
||||||
|
} catch (e) {
|
||||||
|
notifications.error(e.message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
|
|
|
@ -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) {
|
||||||
setMonthlyUsage()
|
setPrimaryActionText()
|
||||||
setStaticUsage()
|
setCancelAt()
|
||||||
|
setTextRows()
|
||||||
|
setDaysRemainingInMonth()
|
||||||
|
|
||||||
|
if (quotaUsage) {
|
||||||
|
setMonthlyUsage()
|
||||||
|
setStaticUsage()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
<Layout>
|
<Layout>
|
||||||
<Heading>Usage</Heading>
|
<Layout noPadding gap="S">
|
||||||
<Body
|
<Heading>Usage</Heading>
|
||||||
>Get information about your current usage within Budibase.
|
<Body
|
||||||
{#if $admin.cloud}
|
>Get information about your current usage within Budibase.
|
||||||
{#if $auth.user?.accountPortalAccess}
|
{#if 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 gap="S">
|
|
||||||
<Divider size="S" />
|
|
||||||
</Layout>
|
|
||||||
<Layout gap="S" noPadding>
|
|
||||||
<Layout gap="XS">
|
|
||||||
<Body size="S">YOUR PLAN</Body>
|
|
||||||
<Heading size="S">{capitalise(license?.plan.type)}</Heading>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
<Layout gap="S">
|
<Divider />
|
||||||
<Body size="S">USAGE</Body>
|
<DashCard
|
||||||
<div class="usages">
|
description="YOUR CURRENT PLAN"
|
||||||
{#each staticUsage as usage}
|
title={planTitle()}
|
||||||
<div class="usage">
|
{primaryActionText}
|
||||||
<Usage {usage} />
|
primaryAction={accountPortalAccess ? goToAccountPortal : undefined}
|
||||||
|
{textRows}
|
||||||
|
>
|
||||||
|
<Layout gap="S" noPadding>
|
||||||
|
<Layout gap="S">
|
||||||
|
<div class="usages">
|
||||||
|
<Layout noPadding>
|
||||||
|
{#each staticUsage as usage}
|
||||||
|
<div class="usage">
|
||||||
|
<Usage
|
||||||
|
{usage}
|
||||||
|
warnWhenFull={WARN_USAGE.includes(usage.name)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
</Layout>
|
||||||
</div>
|
{#if monthlyUsage.length}
|
||||||
</Layout>
|
<div class="monthly-container">
|
||||||
{#if monthlyUsage.length}
|
<Layout gap="S">
|
||||||
<Layout gap="S">
|
<Heading size="S" weight="light">Monthly</Heading>
|
||||||
<Body size="S">MONTHLY</Body>
|
<div class="detail">
|
||||||
<div class="usages">
|
<TooltipWrapper tooltip={new Date(quotaReset)}>
|
||||||
{#each monthlyUsage as usage}
|
<Detail size="M">Resets in {daysRemainingInMonth} days</Detail
|
||||||
<div class="usage">
|
>
|
||||||
<Usage {usage} />
|
</TooltipWrapper>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
<div class="usages">
|
||||||
</div>
|
<Layout noPadding>
|
||||||
|
{#each monthlyUsage as usage}
|
||||||
|
<div class="usage">
|
||||||
|
<Usage
|
||||||
|
{usage}
|
||||||
|
warnWhenFull={WARN_USAGE.includes(usage.name)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</Layout>
|
</Layout>
|
||||||
<div />
|
</DashCard>
|
||||||
{/if}
|
|
||||||
</Layout>
|
</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>
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
const AnalyticsClient = require("./analytics/Client")
|
||||||
|
|
||||||
|
const client = new AnalyticsClient()
|
||||||
|
|
||||||
|
exports.captureEvent = (event, properties) => {
|
||||||
|
client.capture({
|
||||||
|
distinctId: "cli",
|
||||||
|
event,
|
||||||
|
properties,
|
||||||
|
})
|
||||||
|
}
|
|
@ -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",
|
type,
|
||||||
event: AnalyticsEvents.SelfHostInit,
|
|
||||||
properties: {
|
|
||||||
type,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
await downloadFiles()
|
await downloadFiles()
|
||||||
const config = isQuick ? makeEnv.QUICK_CONFIG : {}
|
const config = isQuick ? makeEnv.QUICK_CONFIG : {}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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 {
|
|
@ -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 />
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue