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:
|
||||
install: false
|
||||
command: yarn test:e2e:ci
|
||||
|
||||
- name: QA Core Integration Tests
|
||||
run: |
|
||||
cd qa-core
|
||||
yarn
|
||||
yarn api:test:ci
|
|
@ -63,6 +63,7 @@ typings/
|
|||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
!qa-core/.env
|
||||
!hosting/.env
|
||||
hosting/.generated-nginx.dev.conf
|
||||
hosting/proxy/.generated-nginx.prod.conf
|
||||
|
|
|
@ -8,4 +8,4 @@ packages/server/client
|
|||
packages/server/src/definitions/openapi.ts
|
||||
packages/builder/.routify
|
||||
packages/builder/cypress/support/queryLevelTransformerFunction.js
|
||||
packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js
|
||||
packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js
|
|
@ -65,7 +65,7 @@ Budibase is open-source - licensed as GPL v3. This should fill you with confiden
|
|||
<br /><br />
|
||||
|
||||
### Load data or start from scratch
|
||||
Budibase pulls in data from multiple sources, including MongoDB, CouchDB, PostgreSQL, MySQL, Airtable, S3, DynamoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no data sources. [Request new data sources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||
Budibase pulls in data from multiple sources, including MongoDB, CouchDB, PostgreSQL, MySQL, Airtable, S3, DynamoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no datasources. [Request new datasources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||
|
||||
<p align="center">
|
||||
<img alt="Budibase data" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970242/Out%20of%20beta%20launch/data_n1tlhf.png">
|
||||
|
|
|
@ -348,7 +348,7 @@ export interface paths {
|
|||
}
|
||||
}
|
||||
responses: {
|
||||
/** Returns the created table, including the ID which has been generated for it. This can be internal or external data sources. */
|
||||
/** Returns the created table, including the ID which has been generated for it. This can be internal or external datasources. */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["tableOutput"]
|
||||
|
@ -959,7 +959,7 @@ export interface components {
|
|||
query: {
|
||||
/** @description The ID of the query. */
|
||||
_id: string
|
||||
/** @description The ID of the data source the query belongs to. */
|
||||
/** @description The ID of the datasource the query belongs to. */
|
||||
datasourceId?: string
|
||||
/** @description The bindings which are required to perform this query. */
|
||||
parameters?: string[]
|
||||
|
@ -983,7 +983,7 @@ export interface components {
|
|||
data: {
|
||||
/** @description The ID of the query. */
|
||||
_id: string
|
||||
/** @description The ID of the data source the query belongs to. */
|
||||
/** @description The ID of the datasource the query belongs to. */
|
||||
datasourceId?: string
|
||||
/** @description The bindings which are required to perform this query. */
|
||||
parameters?: string[]
|
||||
|
|
|
@ -9,7 +9,11 @@ if [[ "${TARGETBUILD}" = "aas" ]]; then
|
|||
chown -R couchdb:couchdb $DATA_DIR/couch/
|
||||
apt update
|
||||
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
|
||||
sed -i "s#DATA_DIR#/home#g" /opt/clouseau/clouseau.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 /worker /worker
|
||||
|
||||
ENV \
|
||||
APP_PORT=4001 \
|
||||
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
|
||||
|
||||
# ENV CUSTOM_DOMAIN=budi001.custom.com \
|
||||
# See runner.sh for Env Vars
|
||||
# These secret env variables are generated by the runner at startup
|
||||
# their values can be overriden by the user, they will be written
|
||||
# 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
|
||||
# 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
|
||||
|
||||
# cleanup cache
|
||||
|
@ -124,6 +111,8 @@ RUN yarn cache clean -f
|
|||
|
||||
EXPOSE 80
|
||||
EXPOSE 443
|
||||
# Expose port 2222 for SSH on Azure App Service build
|
||||
EXPOSE 2222
|
||||
VOLUME /data
|
||||
|
||||
# setup letsencrypt certificate
|
||||
|
|
|
@ -1,6 +1,21 @@
|
|||
#!/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 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
|
||||
if [[ "${TARGETBUILD}" = "aas" ]]; then
|
||||
DATA_DIR=/home
|
||||
|
@ -10,9 +25,10 @@ else
|
|||
fi
|
||||
|
||||
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
|
||||
# first randomise any unset environment variables
|
||||
# randomise any unset environment variables
|
||||
for ENV_VAR in "${ENV_VARS[@]}"
|
||||
do
|
||||
temp=$(eval "echo \$$ENV_VAR")
|
||||
|
@ -30,11 +46,18 @@ if [ ! -f "${DATA_DIR}/.env" ]; then
|
|||
temp=$(eval "echo \$$ENV_VAR")
|
||||
echo "$ENV_VAR=$temp" >> ${DATA_DIR}/.env
|
||||
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
|
||||
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
|
||||
mkdir -p ${DATA_DIR}/couch/{dbs,views}
|
||||
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",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -45,8 +45,8 @@
|
|||
"lint:eslint": "eslint packages",
|
||||
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\"",
|
||||
"lint": "yarn run lint:eslint && yarn run lint:prettier",
|
||||
"lint:fix:eslint": "eslint --fix packages",
|
||||
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\"",
|
||||
"lint:fix:eslint": "eslint --fix packages qa-core",
|
||||
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --write \"qa-core/**/*.{js,ts,svelte}\"",
|
||||
"lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint",
|
||||
"test:e2e": "lerna run cy:test --stream",
|
||||
"test:e2e:ci": "lerna run cy:ci --stream",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/backend-core",
|
||||
"version": "1.3.15-alpha.7",
|
||||
"version": "1.3.19-alpha.6",
|
||||
"description": "Budibase backend core libraries used in server and worker",
|
||||
"main": "dist/src/index.js",
|
||||
"types": "dist/src/index.d.ts",
|
||||
|
@ -20,7 +20,7 @@
|
|||
"test:watch": "jest --watchAll"
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/types": "1.3.15-alpha.7",
|
||||
"@budibase/types": "1.3.19-alpha.6",
|
||||
"@shopify/jest-koa-mocks": "5.0.1",
|
||||
"@techpass/passport-openidconnect": "0.3.2",
|
||||
"aws-sdk": "2.1030.0",
|
||||
|
|
|
@ -7,6 +7,7 @@ exports.Cookies = {
|
|||
CurrentApp: "budibase:currentapp",
|
||||
Auth: "budibase:auth",
|
||||
Init: "budibase:init",
|
||||
ACCOUNT_RETURN_URL: "budibase:account:returnurl",
|
||||
DatasourceAuth: "budibase:datasourceauth",
|
||||
OIDC_CONFIG: "budibase:oidc:config",
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ export enum ViewName {
|
|||
ROUTING = "screen_routes",
|
||||
AUTOMATION_LOGS = "automation_logs",
|
||||
ACCOUNT_BY_EMAIL = "account_by_email",
|
||||
PLATFORM_USERS_LOWERCASE = "platform_users_lowercase",
|
||||
}
|
||||
|
||||
export const DeprecatedViews = {
|
||||
|
|
|
@ -2,7 +2,8 @@ import { newid } from "../hashing"
|
|||
import { DEFAULT_TENANT_ID, Configs } from "../constants"
|
||||
import env from "../environment"
|
||||
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 { doWithDB, allDbs } from "./index"
|
||||
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,
|
||||
ACCOUNT_PORTAL_URL:
|
||||
process.env.ACCOUNT_PORTAL_URL || "https://account.budibase.app",
|
||||
ACCOUNT_PORTAL_API_KEY: process.env.ACCOUNT_PORTAL_API_KEY,
|
||||
ACCOUNT_PORTAL_API_KEY: process.env.ACCOUNT_PORTAL_API_KEY || "",
|
||||
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
|
||||
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED || ""),
|
||||
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
class BudibaseError extends Error {
|
||||
constructor(message, code, type) {
|
||||
super(message)
|
||||
this.code = code
|
||||
this.type = type
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
BudibaseError,
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
export class BudibaseError extends Error {
|
||||
code: string
|
||||
type: string
|
||||
|
||||
constructor(message: string, code: string, type: string) {
|
||||
super(message)
|
||||
this.code = code
|
||||
this.type = type
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
const { BudibaseError } = require("./base")
|
||||
|
||||
class GenericError extends BudibaseError {
|
||||
constructor(message, code, type) {
|
||||
super(message, code, type ? type : "generic")
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
GenericError,
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { BudibaseError } from "./base"
|
||||
|
||||
export class GenericError extends BudibaseError {
|
||||
constructor(message: string, code: string, type: string) {
|
||||
super(message, code, type ? type : "generic")
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
const { GenericError } = require("./generic")
|
||||
|
||||
class HTTPError extends GenericError {
|
||||
constructor(message, httpStatus, code = "http", type = "generic") {
|
||||
super(message, code, type)
|
||||
this.status = httpStatus
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
HTTPError,
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { GenericError } from "./generic"
|
||||
|
||||
export class HTTPError extends GenericError {
|
||||
status: number
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
httpStatus: number,
|
||||
code = "http",
|
||||
type = "generic"
|
||||
) {
|
||||
super(message, code, type)
|
||||
this.status = httpStatus
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
const http = require("./http")
|
||||
const licensing = require("./licensing")
|
||||
import { HTTPError } from "./http"
|
||||
import { UsageLimitError, FeatureDisabledError } from "./licensing"
|
||||
import * as licensing from "./licensing"
|
||||
|
||||
const codes = {
|
||||
...licensing.codes,
|
||||
|
@ -11,7 +12,7 @@ const context = {
|
|||
...licensing.context,
|
||||
}
|
||||
|
||||
const getPublicError = err => {
|
||||
const getPublicError = (err: any) => {
|
||||
let error
|
||||
if (err.code || err.type) {
|
||||
// add generic error information
|
||||
|
@ -32,13 +33,15 @@ const getPublicError = err => {
|
|||
return error
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
const pkg = {
|
||||
codes,
|
||||
types,
|
||||
errors: {
|
||||
UsageLimitError: licensing.UsageLimitError,
|
||||
FeatureDisabledError: licensing.FeatureDisabledError,
|
||||
HTTPError: http.HTTPError,
|
||||
UsageLimitError,
|
||||
FeatureDisabledError,
|
||||
HTTPError,
|
||||
},
|
||||
getPublicError,
|
||||
}
|
||||
|
||||
export = pkg
|
|
@ -1,43 +0,0 @@
|
|||
const { HTTPError } = require("./http")
|
||||
|
||||
const type = "license_error"
|
||||
|
||||
const codes = {
|
||||
USAGE_LIMIT_EXCEEDED: "usage_limit_exceeded",
|
||||
FEATURE_DISABLED: "feature_disabled",
|
||||
}
|
||||
|
||||
const context = {
|
||||
[codes.USAGE_LIMIT_EXCEEDED]: err => {
|
||||
return {
|
||||
limitName: err.limitName,
|
||||
}
|
||||
},
|
||||
[codes.FEATURE_DISABLED]: err => {
|
||||
return {
|
||||
featureName: err.featureName,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
class UsageLimitError extends HTTPError {
|
||||
constructor(message, limitName) {
|
||||
super(message, 400, codes.USAGE_LIMIT_EXCEEDED, type)
|
||||
this.limitName = limitName
|
||||
}
|
||||
}
|
||||
|
||||
class FeatureDisabledError extends HTTPError {
|
||||
constructor(message, featureName) {
|
||||
super(message, 400, codes.FEATURE_DISABLED, type)
|
||||
this.featureName = featureName
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
type,
|
||||
codes,
|
||||
context,
|
||||
UsageLimitError,
|
||||
FeatureDisabledError,
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
import { HTTPError } from "./http"
|
||||
|
||||
export const type = "license_error"
|
||||
|
||||
export const codes = {
|
||||
USAGE_LIMIT_EXCEEDED: "usage_limit_exceeded",
|
||||
FEATURE_DISABLED: "feature_disabled",
|
||||
}
|
||||
|
||||
export const context = {
|
||||
[codes.USAGE_LIMIT_EXCEEDED]: (err: any) => {
|
||||
return {
|
||||
limitName: err.limitName,
|
||||
}
|
||||
},
|
||||
[codes.FEATURE_DISABLED]: (err: any) => {
|
||||
return {
|
||||
featureName: err.featureName,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export class UsageLimitError extends HTTPError {
|
||||
limitName: string
|
||||
|
||||
constructor(message: string, limitName: string) {
|
||||
super(message, 400, codes.USAGE_LIMIT_EXCEEDED, type)
|
||||
this.limitName = limitName
|
||||
}
|
||||
}
|
||||
|
||||
export class FeatureDisabledError extends HTTPError {
|
||||
featureName: string
|
||||
|
||||
constructor(message: string, featureName: string) {
|
||||
super(message, 400, codes.FEATURE_DISABLED, type)
|
||||
this.featureName = featureName
|
||||
}
|
||||
}
|
|
@ -5,8 +5,15 @@ import {
|
|||
DatasourceCreatedEvent,
|
||||
DatasourceUpdatedEvent,
|
||||
DatasourceDeletedEvent,
|
||||
SourceName,
|
||||
} from "@budibase/types"
|
||||
|
||||
function isCustom(datasource: Datasource) {
|
||||
const sources = Object.values(SourceName)
|
||||
// if not in the base source list, then it must be custom
|
||||
return !sources.includes(datasource.source)
|
||||
}
|
||||
|
||||
export async function created(
|
||||
datasource: Datasource,
|
||||
timestamp?: string | number
|
||||
|
@ -14,6 +21,7 @@ export async function created(
|
|||
const properties: DatasourceCreatedEvent = {
|
||||
datasourceId: datasource._id as string,
|
||||
source: datasource.source,
|
||||
custom: isCustom(datasource),
|
||||
}
|
||||
await publishEvent(Event.DATASOURCE_CREATED, properties, timestamp)
|
||||
}
|
||||
|
@ -22,6 +30,7 @@ export async function updated(datasource: Datasource) {
|
|||
const properties: DatasourceUpdatedEvent = {
|
||||
datasourceId: datasource._id as string,
|
||||
source: datasource.source,
|
||||
custom: isCustom(datasource),
|
||||
}
|
||||
await publishEvent(Event.DATASOURCE_UPDATED, properties)
|
||||
}
|
||||
|
@ -30,6 +39,7 @@ export async function deleted(datasource: Datasource) {
|
|||
const properties: DatasourceDeletedEvent = {
|
||||
datasourceId: datasource._id as string,
|
||||
source: datasource.source,
|
||||
custom: isCustom(datasource),
|
||||
}
|
||||
await publishEvent(Event.DATASOURCE_DELETED, properties)
|
||||
}
|
||||
|
|
|
@ -18,3 +18,4 @@ export * as view from "./view"
|
|||
export * as installation from "./installation"
|
||||
export * as backfill from "./backfill"
|
||||
export * as group from "./group"
|
||||
export * as plugin from "./plugin"
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
import { publishEvent } from "../events"
|
||||
import {
|
||||
Event,
|
||||
Plugin,
|
||||
PluginDeletedEvent,
|
||||
PluginImportedEvent,
|
||||
PluginInitEvent,
|
||||
} from "@budibase/types"
|
||||
|
||||
export async function init(plugin: Plugin) {
|
||||
const properties: PluginInitEvent = {
|
||||
type: plugin.schema.type,
|
||||
name: plugin.name,
|
||||
description: plugin.description,
|
||||
version: plugin.version,
|
||||
}
|
||||
await publishEvent(Event.PLUGIN_INIT, properties)
|
||||
}
|
||||
|
||||
export async function imported(plugin: Plugin) {
|
||||
const properties: PluginImportedEvent = {
|
||||
pluginId: plugin._id as string,
|
||||
type: plugin.schema.type,
|
||||
source: plugin.source,
|
||||
name: plugin.name,
|
||||
description: plugin.description,
|
||||
version: plugin.version,
|
||||
}
|
||||
await publishEvent(Event.PLUGIN_IMPORTED, properties)
|
||||
}
|
||||
|
||||
export async function deleted(plugin: Plugin) {
|
||||
const properties: PluginDeletedEvent = {
|
||||
pluginId: plugin._id as string,
|
||||
type: plugin.schema.type,
|
||||
name: plugin.name,
|
||||
description: plugin.description,
|
||||
version: plugin.version,
|
||||
}
|
||||
await publishEvent(Event.PLUGIN_DELETED, properties)
|
||||
}
|
|
@ -31,20 +31,26 @@ const TENANT_FEATURE_FLAGS = getFeatureFlags()
|
|||
|
||||
exports.isEnabled = featureFlag => {
|
||||
const tenantId = tenancy.getTenantId()
|
||||
|
||||
return (
|
||||
TENANT_FEATURE_FLAGS &&
|
||||
TENANT_FEATURE_FLAGS[tenantId] &&
|
||||
TENANT_FEATURE_FLAGS[tenantId].includes(featureFlag)
|
||||
)
|
||||
const flags = exports.getTenantFeatureFlags(tenantId)
|
||||
return flags.includes(featureFlag)
|
||||
}
|
||||
|
||||
exports.getTenantFeatureFlags = tenantId => {
|
||||
if (TENANT_FEATURE_FLAGS && TENANT_FEATURE_FLAGS[tenantId]) {
|
||||
return TENANT_FEATURE_FLAGS[tenantId]
|
||||
const flags = []
|
||||
|
||||
if (TENANT_FEATURE_FLAGS) {
|
||||
const globalFlags = TENANT_FEATURE_FLAGS["*"]
|
||||
const tenantFlags = TENANT_FEATURE_FLAGS[tenantId]
|
||||
|
||||
if (globalFlags) {
|
||||
flags.push(...globalFlags)
|
||||
}
|
||||
if (tenantFlags) {
|
||||
flags.push(...tenantFlags)
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
return flags
|
||||
}
|
||||
|
||||
exports.FeatureFlag = {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import errors from "./errors"
|
||||
|
||||
const errorClasses = errors.errors
|
||||
import * as events from "./events"
|
||||
import * as migrations from "./migrations"
|
||||
|
@ -15,7 +14,7 @@ import deprovisioning from "./context/deprovision"
|
|||
import auth from "./auth"
|
||||
import constants from "./constants"
|
||||
import * as dbConstants from "./db/constants"
|
||||
import logging from "./logging"
|
||||
import * as logging from "./logging"
|
||||
import pino from "./pino"
|
||||
import * as middleware from "./middleware"
|
||||
import plugins from "./plugin"
|
||||
|
|
|
@ -106,6 +106,7 @@ export = (
|
|||
user = await getUser(userId, session.tenantId)
|
||||
}
|
||||
user.csrfToken = session.csrfToken
|
||||
|
||||
if (session?.lastAccessedAt < timeMinusOneMinute()) {
|
||||
// make sure we denote that the session is still in use
|
||||
await updateSessionTTL(session)
|
||||
|
|
|
@ -17,14 +17,6 @@ export const DEFINITIONS: MigrationDefinition[] = [
|
|||
type: MigrationType.APP,
|
||||
name: MigrationName.APP_URLS,
|
||||
},
|
||||
{
|
||||
type: MigrationType.GLOBAL,
|
||||
name: MigrationName.DEVELOPER_QUOTA,
|
||||
},
|
||||
{
|
||||
type: MigrationType.GLOBAL,
|
||||
name: MigrationName.PUBLISHED_APP_QUOTA,
|
||||
},
|
||||
{
|
||||
type: MigrationType.APP,
|
||||
name: MigrationName.EVENT_APP_BACKFILL,
|
||||
|
|
|
@ -2,28 +2,12 @@ const redis = require("../redis/init")
|
|||
const { v4: uuidv4 } = require("uuid")
|
||||
const { logWarn } = require("../logging")
|
||||
const env = require("../environment")
|
||||
|
||||
interface CreateSession {
|
||||
sessionId: string
|
||||
tenantId: string
|
||||
csrfToken?: string
|
||||
}
|
||||
|
||||
interface Session extends CreateSession {
|
||||
userId: string
|
||||
lastAccessedAt: string
|
||||
createdAt: string
|
||||
// make optional attributes required
|
||||
csrfToken: string
|
||||
}
|
||||
|
||||
interface SessionKey {
|
||||
key: string
|
||||
}
|
||||
|
||||
interface ScannedSession {
|
||||
value: Session
|
||||
}
|
||||
import {
|
||||
Session,
|
||||
ScannedSession,
|
||||
SessionKey,
|
||||
CreateSession,
|
||||
} from "@budibase/types"
|
||||
|
||||
// a week in seconds
|
||||
const EXPIRY_SECONDS = 86400 * 7
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import * as context from "../context"
|
||||
import * as tenancy from "./tenancy"
|
||||
import * as utils from "./utils"
|
||||
|
||||
const pkg = {
|
||||
...context,
|
||||
...tenancy,
|
||||
...utils,
|
||||
}
|
||||
|
||||
export = pkg
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { doWithDB } from "../db"
|
||||
import { StaticDatabases } from "../db/constants"
|
||||
import { baseGlobalDBName } from "./utils"
|
||||
import { queryPlatformView } from "../db/views"
|
||||
import { StaticDatabases, ViewName } from "../db/constants"
|
||||
import { getGlobalDBName } from "./utils"
|
||||
import {
|
||||
getTenantId,
|
||||
DEFAULT_TENANT_ID,
|
||||
|
@ -8,6 +9,7 @@ import {
|
|||
getTenantIDFromAppID,
|
||||
} from "../context"
|
||||
import env from "../environment"
|
||||
import { PlatformUser, PlatformUserByEmail } from "@budibase/types"
|
||||
|
||||
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants
|
||||
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) => {
|
||||
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
|
||||
export const getTenantUser = async (identifier: string) => {
|
||||
return doWithDB(PLATFORM_INFO_DB, async (db: any) => {
|
||||
try {
|
||||
return await db.get(identifier)
|
||||
} catch (err) {
|
||||
return null
|
||||
}
|
||||
})
|
||||
export const getTenantUser = async (
|
||||
identifier: string
|
||||
): Promise<PlatformUser | null> => {
|
||||
// use the view here and allow to find anyone regardless of casing
|
||||
// Use lowercase to ensure email login is case insensitive
|
||||
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) => {
|
||||
|
|
|
@ -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,
|
||||
getUsersByAppParams,
|
||||
getProdAppID,
|
||||
generateAppUserID,
|
||||
} = require("./db/utils")
|
||||
const { queryGlobalView } = require("./db/views")
|
||||
const { UNICODE_MAX } = require("./db/constants")
|
||||
} from "./db/utils"
|
||||
import { queryGlobalView } from "./db/views"
|
||||
import { UNICODE_MAX } from "./db/constants"
|
||||
import { User } from "@budibase/types"
|
||||
|
||||
/**
|
||||
* Given an email address this will use a view to search through
|
||||
* all the users to find one with this email address.
|
||||
* @param {string} email the email to lookup the user by.
|
||||
*/
|
||||
exports.getGlobalUserByEmail = async email => {
|
||||
export const getGlobalUserByEmail = async (
|
||||
email: String
|
||||
): Promise<User | undefined> => {
|
||||
if (email == null) {
|
||||
throw "Must supply an email address to view"
|
||||
}
|
||||
|
||||
return await queryGlobalView(ViewName.USER_BY_EMAIL, {
|
||||
const response = await queryGlobalView<User>(ViewName.USER_BY_EMAIL, {
|
||||
key: email.toLowerCase(),
|
||||
include_docs: true,
|
||||
})
|
||||
|
||||
if (Array.isArray(response)) {
|
||||
// shouldn't be able to happen, but need to handle just in case
|
||||
throw new Error(`Multiple users found with email address: ${email}`)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
exports.searchGlobalUsersByApp = async (appId, opts) => {
|
||||
export const searchGlobalUsersByApp = async (appId: any, opts: any) => {
|
||||
if (typeof appId !== "string") {
|
||||
throw new Error("Must provide a string based app ID")
|
||||
}
|
||||
|
@ -38,24 +48,24 @@ exports.searchGlobalUsersByApp = async (appId, opts) => {
|
|||
return Array.isArray(response) ? response : [response]
|
||||
}
|
||||
|
||||
exports.getGlobalUserByAppPage = (appId, user) => {
|
||||
export const getGlobalUserByAppPage = (appId: string, user: User) => {
|
||||
if (!user) {
|
||||
return
|
||||
}
|
||||
return generateAppUserID(getProdAppID(appId), user._id)
|
||||
return generateAppUserID(getProdAppID(appId), user._id!)
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a starts with search on the global email view.
|
||||
*/
|
||||
exports.searchGlobalUsersByEmail = async (email, opts) => {
|
||||
export const searchGlobalUsersByEmail = async (email: string, opts: any) => {
|
||||
if (typeof email !== "string") {
|
||||
throw new Error("Must provide a string to search by")
|
||||
}
|
||||
const lcEmail = email.toLowerCase()
|
||||
// handle if passing up startkey for pagination
|
||||
const startkey = opts && opts.startkey ? opts.startkey : lcEmail
|
||||
let response = await queryGlobalView(ViewName.USER_BY_EMAIL, {
|
||||
let response = await queryGlobalView<User>(ViewName.USER_BY_EMAIL, {
|
||||
...opts,
|
||||
startkey,
|
||||
endkey: `${lcEmail}${UNICODE_MAX}`,
|
|
@ -42,6 +42,18 @@ async function resolveAppUrl(ctx) {
|
|||
return app && app.appId ? app.appId : undefined
|
||||
}
|
||||
|
||||
exports.isServingApp = ctx => {
|
||||
// dev app
|
||||
if (ctx.path.startsWith(`/${APP_PREFIX}`)) {
|
||||
return true
|
||||
}
|
||||
// prod app
|
||||
if (ctx.path.startsWith(PROD_APP_PREFIX)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a request tries to find the appId, which can be located in various places
|
||||
* @param {object} ctx The main request body to look through.
|
||||
|
|
|
@ -1377,6 +1377,11 @@ bcrypt@5.0.1:
|
|||
"@mapbox/node-pre-gyp" "^1.0.0"
|
||||
node-addon-api "^3.1.0"
|
||||
|
||||
bcryptjs@2.4.3:
|
||||
version "2.4.3"
|
||||
resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb"
|
||||
integrity sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==
|
||||
|
||||
binary-extensions@^2.0.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/bbui",
|
||||
"description": "A UI solution used in the different Budibase projects.",
|
||||
"version": "1.3.15-alpha.7",
|
||||
"version": "1.3.19-alpha.6",
|
||||
"license": "MPL-2.0",
|
||||
"svelte": "src/index.js",
|
||||
"module": "dist/bbui.es.js",
|
||||
|
@ -38,7 +38,7 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
|
||||
"@budibase/string-templates": "1.3.15-alpha.7",
|
||||
"@budibase/string-templates": "1.3.19-alpha.6",
|
||||
"@spectrum-css/actionbutton": "^1.0.1",
|
||||
"@spectrum-css/actiongroup": "^1.0.1",
|
||||
"@spectrum-css/avatar": "^3.0.2",
|
||||
|
|
|
@ -4,22 +4,32 @@
|
|||
import { banner } from "../Stores/banner"
|
||||
import Banner from "./Banner.svelte"
|
||||
import { fly } from "svelte/transition"
|
||||
import TooltipWrapper from "../Tooltip/TooltipWrapper.svelte"
|
||||
</script>
|
||||
|
||||
<Portal target=".banner-container">
|
||||
<div class="banner">
|
||||
{#if $banner.message}
|
||||
{#each $banner.messages as message}
|
||||
<div transition:fly={{ y: -30 }}>
|
||||
<Banner
|
||||
type={$banner.type}
|
||||
extraButtonText={$banner.extraButtonText}
|
||||
extraButtonAction={$banner.extraButtonAction}
|
||||
on:change={$banner.onChange}
|
||||
type={message.type}
|
||||
extraButtonText={message.extraButtonText}
|
||||
extraButtonAction={message.extraButtonAction}
|
||||
on:change={() => {
|
||||
if (message.onChange) {
|
||||
message.onChange()
|
||||
}
|
||||
}}
|
||||
showCloseButton={typeof message.showCloseButton === "boolean"
|
||||
? message.showCloseButton
|
||||
: true}
|
||||
>
|
||||
{$banner.message}
|
||||
<TooltipWrapper tooltip={message.tooltip} disabled={false}>
|
||||
{message.message}
|
||||
</TooltipWrapper>
|
||||
</Banner>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</Portal>
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
export let header = ""
|
||||
export let message = ""
|
||||
export let onConfirm = undefined
|
||||
export let buttonText = ""
|
||||
|
||||
$: icon = selectIcon(type)
|
||||
// if newlines used, convert them to different elements
|
||||
|
@ -39,13 +40,16 @@
|
|||
<div class="spectrum-InLineAlert-content">{splitMsg}</div>
|
||||
{/each}
|
||||
{#if onConfirm}
|
||||
<div class="spectrum-InLineAlert-footer">
|
||||
<Button secondary on:click={onConfirm}>OK</Button>
|
||||
<div class="spectrum-InLineAlert-footer button">
|
||||
<Button secondary on:click={onConfirm}>{buttonText || "OK"}</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.spectrum-InLineAlert {
|
||||
--spectrum-semantic-negative-border-color: #e34850;
|
||||
--spectrum-semantic-positive-border-color: #2d9d78;
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
export let duration = 1000
|
||||
export let width = false
|
||||
export let sideLabel = false
|
||||
export let hidePercentage = true
|
||||
export let color // red, green, default = blue
|
||||
|
||||
export let size = "M"
|
||||
|
||||
|
@ -37,7 +39,7 @@
|
|||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
{#if value || value === 0}
|
||||
{#if !hidePercentage && (value || value === 0)}
|
||||
<div
|
||||
class="spectrum-FieldLabel spectrum-ProgressBar-percentage spectrum-FieldLabel--size{size}"
|
||||
>
|
||||
|
@ -47,8 +49,19 @@
|
|||
<div class="spectrum-ProgressBar-track">
|
||||
<div
|
||||
class="spectrum-ProgressBar-fill"
|
||||
class:color-green={color === "green"}
|
||||
class:color-red={color === "red"}
|
||||
style={value || value === 0 ? `width: ${$progress}%` : ""}
|
||||
/>
|
||||
</div>
|
||||
<div class="spectrum-ProgressBar-label" hidden="" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.color-green {
|
||||
background: #009562;
|
||||
}
|
||||
.color-red {
|
||||
background: #dd2019;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
const multilevel = getContext("sidenav-type")
|
||||
import Badge from "../Badge/Badge.svelte"
|
||||
export let href = ""
|
||||
export let external = false
|
||||
export let heading = ""
|
||||
|
@ -8,6 +9,7 @@
|
|||
export let selected = false
|
||||
export let disabled = false
|
||||
export let dataCy
|
||||
export let badge = ""
|
||||
</script>
|
||||
|
||||
<li
|
||||
|
@ -38,10 +40,22 @@
|
|||
</svg>
|
||||
{/if}
|
||||
<slot />
|
||||
{#if badge}
|
||||
<div class="badge">
|
||||
<Badge active size="S">{badge}</Badge>
|
||||
</div>
|
||||
{/if}
|
||||
</a>
|
||||
|
||||
{#if multilevel && $$slots.subnav}
|
||||
<ul class="spectrum-SideNav">
|
||||
<slot name="subnav" />
|
||||
</ul>
|
||||
{/if}
|
||||
</li>
|
||||
|
||||
<style>
|
||||
.badge {
|
||||
margin-left: 10px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
import { writable } from "svelte/store"
|
||||
|
||||
export const BANNER_TYPES = {
|
||||
INFO: "info",
|
||||
NEGATIVE: "negative",
|
||||
}
|
||||
|
||||
export function createBannerStore() {
|
||||
const DEFAULT_CONFIG = {}
|
||||
const DEFAULT_CONFIG = {
|
||||
messages: [],
|
||||
}
|
||||
|
||||
const banner = writable(DEFAULT_CONFIG)
|
||||
|
||||
|
@ -20,17 +27,38 @@ export function createBannerStore() {
|
|||
const showStatus = async () => {
|
||||
const config = {
|
||||
message: "Some systems are experiencing issues",
|
||||
type: "negative",
|
||||
type: BANNER_TYPES.NEGATIVE,
|
||||
extraButtonText: "View Status",
|
||||
extraButtonAction: () => window.open("https://status.budibase.com/"),
|
||||
}
|
||||
|
||||
await show(config)
|
||||
await queue([config])
|
||||
}
|
||||
|
||||
const queue = async entries => {
|
||||
const priority = {
|
||||
[BANNER_TYPES.NEGATIVE]: 0,
|
||||
[BANNER_TYPES.INFO]: 1,
|
||||
}
|
||||
banner.update(store => {
|
||||
const sorted = [...store.messages, ...entries].sort((a, b) => {
|
||||
if (priority[a.type] == priority[b.type]) {
|
||||
return 0
|
||||
}
|
||||
return priority[a.type] < priority[b.type] ? -1 : 1
|
||||
})
|
||||
return {
|
||||
...store,
|
||||
messages: sorted,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: banner.subscribe,
|
||||
showStatus,
|
||||
show,
|
||||
queue,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
export let tooltip = ""
|
||||
export let size = "M"
|
||||
export let disabled = true
|
||||
|
||||
let showTooltip = false
|
||||
</script>
|
||||
|
@ -19,7 +20,7 @@
|
|||
on:mouseleave={() => (showTooltip = false)}
|
||||
on:focus
|
||||
>
|
||||
<Icon name="InfoOutline" size="S" disabled={true} />
|
||||
<Icon name="InfoOutline" size="S" {disabled} />
|
||||
</div>
|
||||
{#if showTooltip}
|
||||
<div class="tooltip">
|
||||
|
@ -54,7 +55,6 @@
|
|||
transform: scale(0.75);
|
||||
}
|
||||
.icon-small {
|
||||
margin-top: -2px;
|
||||
margin-bottom: -5px;
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -34,6 +34,7 @@ export { default as Layout } from "./Layout/Layout.svelte"
|
|||
export { default as Page } from "./Layout/Page.svelte"
|
||||
export { default as Link } from "./Link/Link.svelte"
|
||||
export { default as Tooltip } from "./Tooltip/Tooltip.svelte"
|
||||
export { default as TooltipWrapper } from "./Tooltip/TooltipWrapper.svelte"
|
||||
export { default as Menu } from "./Menu/Menu.svelte"
|
||||
export { default as MenuSection } from "./Menu/Section.svelte"
|
||||
export { default as MenuSeparator } from "./Menu/Separator.svelte"
|
||||
|
@ -94,7 +95,7 @@ export { default as clickOutside } from "./Actions/click_outside"
|
|||
|
||||
// Stores
|
||||
export { notifications, createNotificationStore } from "./Stores/notifications"
|
||||
export { banner } from "./Stores/banner"
|
||||
export { banner, BANNER_TYPES } from "./Stores/banner"
|
||||
|
||||
// Helpers
|
||||
export * as Helpers from "./helpers"
|
||||
|
|
|
@ -82,10 +82,10 @@ filterTests(['smoke', 'all'], () => {
|
|||
})
|
||||
|
||||
if (Cypress.env("TEST_ENV")) {
|
||||
it("should generate data source screens", () => {
|
||||
// Using MySQL data source for testing this
|
||||
it("should generate datasource screens", () => {
|
||||
// Using MySQL datasource for testing this
|
||||
const datasource = "MySQL"
|
||||
// Select & configure MySQL data source
|
||||
// Select & configure MySQL datasource
|
||||
cy.selectExternalDatasource(datasource)
|
||||
cy.addDatasourceConfig(datasource)
|
||||
// Create Autogenerated screens from a MySQL table - MySQL contains books table
|
||||
|
|
|
@ -11,8 +11,8 @@ filterTests(["all"], () => {
|
|||
const queryName = "Cypress Test Query"
|
||||
const queryRename = "CT Query Rename"
|
||||
|
||||
it("Should add MySQL data source without configuration", () => {
|
||||
// Select MySQL data source
|
||||
it("Should add MySQL datasource without configuration", () => {
|
||||
// Select MySQL datasource
|
||||
cy.selectExternalDatasource(datasource)
|
||||
// Attempt to fetch tables without applying configuration
|
||||
cy.intercept("**/datasources").as("datasource")
|
||||
|
@ -35,8 +35,8 @@ filterTests(["all"], () => {
|
|||
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true })
|
||||
})
|
||||
|
||||
it("should add MySQL data source and fetch tables", () => {
|
||||
// Add & configure MySQL data source
|
||||
it("should add MySQL datasource and fetch tables", () => {
|
||||
// Add & configure MySQL datasource
|
||||
cy.selectExternalDatasource(datasource)
|
||||
cy.intercept("**/datasources").as("datasource")
|
||||
cy.addDatasourceConfig(datasource)
|
||||
|
@ -52,7 +52,7 @@ filterTests(["all"], () => {
|
|||
})
|
||||
|
||||
it("should check table fetching error", () => {
|
||||
// MySQL test data source contains tables without primary keys
|
||||
// MySQL test datasource contains tables without primary keys
|
||||
cy.get(".spectrum-InLineAlert")
|
||||
.should("contain", "Error fetching tables")
|
||||
.and("contain", "No primary key constraint found")
|
||||
|
|
|
@ -11,8 +11,8 @@ filterTests(["all"], () => {
|
|||
const queryName = "Cypress Test Query"
|
||||
const queryRename = "CT Query Rename"
|
||||
|
||||
it("Should add Oracle data source and skip table fetch", () => {
|
||||
// Select Oracle data source
|
||||
it("Should add Oracle datasource and skip table fetch", () => {
|
||||
// Select Oracle datasource
|
||||
cy.selectExternalDatasource(datasource)
|
||||
// Skip table fetch - no config added
|
||||
cy.get(".spectrum-Button")
|
||||
|
@ -23,7 +23,7 @@ filterTests(["all"], () => {
|
|||
cy.get(".spectrum-Textfield-input", { timeout: 500 })
|
||||
.eq(1)
|
||||
.should("have.value", "localhost")
|
||||
// Add another Oracle data source, configure & skip table fetch
|
||||
// Add another Oracle datasource, configure & skip table fetch
|
||||
cy.selectExternalDatasource(datasource)
|
||||
cy.addDatasourceConfig(datasource, true)
|
||||
// Confirm config and no tables
|
||||
|
@ -33,8 +33,8 @@ filterTests(["all"], () => {
|
|||
cy.get(".spectrum-Body").eq(2).should("contain", "No tables found.")
|
||||
})
|
||||
|
||||
it("Should add Oracle data source and fetch tables without configuration", () => {
|
||||
// Select Oracle data source
|
||||
it("Should add Oracle datasource and fetch tables without configuration", () => {
|
||||
// Select Oracle datasource
|
||||
cy.selectExternalDatasource(datasource)
|
||||
// Attempt to fetch tables without applying configuration
|
||||
cy.intercept("**/datasources").as("datasource")
|
||||
|
@ -49,8 +49,8 @@ filterTests(["all"], () => {
|
|||
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true })
|
||||
})
|
||||
|
||||
xit("should add Oracle data source and fetch tables", () => {
|
||||
// Add & configure Oracle data source
|
||||
xit("should add Oracle datasource and fetch tables", () => {
|
||||
// Add & configure Oracle datasource
|
||||
cy.selectExternalDatasource(datasource)
|
||||
cy.intercept("**/datasources").as("datasource")
|
||||
cy.addDatasourceConfig(datasource)
|
||||
|
|
|
@ -11,8 +11,8 @@ filterTests(["all"], () => {
|
|||
const queryName = "Cypress Test Query"
|
||||
const queryRename = "CT Query Rename"
|
||||
|
||||
xit("Should add PostgreSQL data source without configuration", () => {
|
||||
// Select PostgreSQL data source
|
||||
xit("Should add PostgreSQL datasource without configuration", () => {
|
||||
// Select PostgreSQL datasource
|
||||
cy.selectExternalDatasource(datasource)
|
||||
// Attempt to fetch tables without applying configuration
|
||||
cy.intercept("**/datasources").as("datasource")
|
||||
|
@ -27,8 +27,8 @@ filterTests(["all"], () => {
|
|||
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true })
|
||||
})
|
||||
|
||||
it("should add PostgreSQL data source and fetch tables", () => {
|
||||
// Add & configure PostgreSQL data source
|
||||
it("should add PostgreSQL datasource and fetch tables", () => {
|
||||
// Add & configure PostgreSQL datasource
|
||||
cy.selectExternalDatasource(datasource)
|
||||
cy.intercept("**/datasources").as("datasource")
|
||||
cy.addDatasourceConfig(datasource)
|
||||
|
|
|
@ -10,8 +10,8 @@ filterTests(["smoke", "all"], () => {
|
|||
const datasource = "REST"
|
||||
const restUrl = "https://api.openbrewerydb.org/breweries"
|
||||
|
||||
it("Should add REST data source with incorrect API", () => {
|
||||
// Select REST data source
|
||||
it("Should add REST datasource with incorrect API", () => {
|
||||
// Select REST datasource
|
||||
cy.selectExternalDatasource(datasource)
|
||||
// Enter incorrect api & attempt to send query
|
||||
cy.get(".query-buttons", { timeout: 1000 }).contains("Add query").click({ force: true })
|
||||
|
|
|
@ -763,7 +763,7 @@ Cypress.Commands.add("navigateToDataSection", () => {
|
|||
})
|
||||
|
||||
Cypress.Commands.add("navigateToAutogeneratedModal", () => {
|
||||
// Screen name must already exist within data source
|
||||
// Screen name must already exist within datasource
|
||||
cy.contains("Design").click()
|
||||
cy.get(".spectrum-Button").contains("Add screen").click({ force: true })
|
||||
cy.get(".spectrum-Modal").within(() => {
|
||||
|
@ -779,7 +779,7 @@ Cypress.Commands.add("navigateToAutogeneratedModal", () => {
|
|||
Cypress.Commands.add("selectExternalDatasource", datasourceName => {
|
||||
// Navigates to Data Section
|
||||
cy.navigateToDataSection()
|
||||
// Open Data Source modal
|
||||
// Open Datasource modal
|
||||
cy.get(".nav").within(() => {
|
||||
cy.get(".add-button").click()
|
||||
})
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/builder",
|
||||
"version": "1.3.15-alpha.7",
|
||||
"version": "1.3.19-alpha.6",
|
||||
"license": "GPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -69,10 +69,10 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "1.3.15-alpha.7",
|
||||
"@budibase/client": "1.3.15-alpha.7",
|
||||
"@budibase/frontend-core": "1.3.15-alpha.7",
|
||||
"@budibase/string-templates": "1.3.15-alpha.7",
|
||||
"@budibase/bbui": "1.3.19-alpha.6",
|
||||
"@budibase/client": "1.3.19-alpha.6",
|
||||
"@budibase/frontend-core": "1.3.19-alpha.6",
|
||||
"@budibase/string-templates": "1.3.19-alpha.6",
|
||||
"@sentry/browser": "5.19.1",
|
||||
"@spectrum-css/page": "^3.0.1",
|
||||
"@spectrum-css/vars": "^3.0.1",
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import { NotificationDisplay, BannerDisplay } from "@budibase/bbui"
|
||||
import { parse, stringify } from "qs"
|
||||
import HelpIcon from "components/common/HelpIcon.svelte"
|
||||
import LicensingOverlays from "components/portal/licensing/LicensingOverlays.svelte"
|
||||
|
||||
const queryHandler = { parse, stringify }
|
||||
</script>
|
||||
|
@ -12,6 +13,9 @@
|
|||
<BannerDisplay />
|
||||
|
||||
<NotificationDisplay />
|
||||
|
||||
<LicensingOverlays />
|
||||
|
||||
<Router {routes} config={{ queryHandler }} />
|
||||
<div class="modal-container" />
|
||||
<HelpIcon />
|
||||
|
|
|
@ -27,7 +27,7 @@ export async function saveDatasource(config, skipFetch = false) {
|
|||
// Create datasource
|
||||
const resp = await datasources.save(datasource, !skipFetch && datasource.plus)
|
||||
|
||||
// update the tables incase data source plus
|
||||
// update the tables incase datasource plus
|
||||
await tables.fetch()
|
||||
await datasources.select(resp._id)
|
||||
return resp
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { getFrontendStore } from "./store/frontend"
|
||||
import { getAutomationStore } from "./store/automation"
|
||||
import { getTemporalStore } from "./store/temporal"
|
||||
import { getThemeStore } from "./store/theme"
|
||||
import { derived } from "svelte/store"
|
||||
import { findComponent, findComponentPath } from "./componentUtils"
|
||||
|
@ -8,6 +9,7 @@ import { RoleUtils } from "@budibase/frontend-core"
|
|||
export const store = getFrontendStore()
|
||||
export const automationStore = getAutomationStore()
|
||||
export const themeStore = getThemeStore()
|
||||
export const temporalStore = getTemporalStore()
|
||||
|
||||
export const selectedScreen = derived(store, $store => {
|
||||
return $store.screens.find(screen => screen._id === $store.selectedScreenId)
|
||||
|
|
|
@ -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"
|
||||
{bindings}
|
||||
fillWidth={true}
|
||||
allowJS={false}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
@ -32,8 +32,8 @@
|
|||
: []
|
||||
$: openDataSource = enrichedDataSources.find(x => x.open)
|
||||
$: {
|
||||
// Ensure the open data source is always included in the list of open
|
||||
// data sources
|
||||
// Ensure the open datasource is always included in the list of open
|
||||
// datasources
|
||||
if (openDataSource) {
|
||||
openNode(openDataSource)
|
||||
}
|
||||
|
@ -79,7 +79,7 @@
|
|||
})
|
||||
|
||||
const containsActiveEntity = datasource => {
|
||||
// If we're view a query then the data source ID is in the URL
|
||||
// If we're view a query then the datasource ID is in the URL
|
||||
if ($params.selectedDatasource === datasource._id) {
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -126,7 +126,7 @@
|
|||
<Modal bind:this={modal}>
|
||||
<ModalContent
|
||||
disabled={!Object.keys(integration).length}
|
||||
title="Add data source"
|
||||
title="Add datasource"
|
||||
confirmText="Continue"
|
||||
showSecondaryButton={showImportButton}
|
||||
secondaryButtonText="Import"
|
||||
|
@ -155,7 +155,7 @@
|
|||
</Layout>
|
||||
|
||||
<Layout noPadding gap="XS">
|
||||
<Body size="S">Connect to an external data source</Body>
|
||||
<Body size="S">Connect to an external datasource</Body>
|
||||
<div class="item-list">
|
||||
{#each Object.entries(integrations).filter(([key, val]) => key !== IntegrationTypes.INTERNAL && !val.custom) as [integrationType, schema]}
|
||||
<DatasourceCard
|
||||
|
@ -170,7 +170,7 @@
|
|||
|
||||
{#if customIntegrations.length > 0}
|
||||
<Layout noPadding gap="XS">
|
||||
<Body size="S">Custom data source</Body>
|
||||
<Body size="S">Custom datasource</Body>
|
||||
<div class="item-list">
|
||||
{#each customIntegrations as [integrationType, schema]}
|
||||
<DatasourceCard
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
} from "@budibase/bbui"
|
||||
import TemplateCard from "components/common/TemplateCard.svelte"
|
||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||
import { licensing } from "stores/portal"
|
||||
|
||||
export let templates
|
||||
|
||||
|
@ -96,15 +97,17 @@
|
|||
backgroundColour={templateEntry.background}
|
||||
icon={templateEntry.icon}
|
||||
>
|
||||
<Button
|
||||
cta
|
||||
on:click={() => {
|
||||
template = templateEntry
|
||||
creationModal.show()
|
||||
}}
|
||||
>
|
||||
Use template
|
||||
</Button>
|
||||
{#if $licensing?.usageMetrics?.apps < 100}
|
||||
<Button
|
||||
cta
|
||||
on:click={() => {
|
||||
template = templateEntry
|
||||
creationModal.show()
|
||||
}}
|
||||
>
|
||||
Use template
|
||||
</Button>
|
||||
{/if}
|
||||
<a
|
||||
href={templateEntry.url}
|
||||
target="_blank"
|
||||
|
|
|
@ -73,13 +73,13 @@
|
|||
|
||||
<div class="root">
|
||||
<Body size="S">
|
||||
Choose the data source that provides the row you would like to duplicate.
|
||||
Choose the datasource that provides the row you would like to duplicate.
|
||||
<br />
|
||||
You can always add or override fields manually.
|
||||
</Body>
|
||||
|
||||
<div class="params">
|
||||
<Label small>Data Source</Label>
|
||||
<Label small>Datasource</Label>
|
||||
<Select
|
||||
bind:value={parameters.providerId}
|
||||
options={providerOptions}
|
||||
|
|
|
@ -71,13 +71,13 @@
|
|||
|
||||
<div class="root">
|
||||
<Body size="S">
|
||||
Choosing a Data Source will automatically use the data it provides, but it's
|
||||
Choosing a Datasource will automatically use the data it provides, but it's
|
||||
optional.<br />
|
||||
You can always add or override fields manually.
|
||||
</Body>
|
||||
|
||||
<div class="params">
|
||||
<Label small>Data Source</Label>
|
||||
<Label small>Datasource</Label>
|
||||
<Select
|
||||
bind:value={parameters.providerId}
|
||||
options={providerOptions}
|
||||
|
|
|
@ -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>
|
||||
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"
|
||||
export let usage
|
||||
export let warnWhenFull = false
|
||||
|
||||
let percentage
|
||||
let unlimited = false
|
||||
let showWarning = false
|
||||
|
||||
$: accountPortalAccess = $auth?.user?.accountPortalAccess
|
||||
|
||||
const isUnlimited = () => {
|
||||
if (usage.total === -1) {
|
||||
|
@ -14,29 +19,62 @@
|
|||
}
|
||||
|
||||
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(() => {
|
||||
unlimited = isUnlimited()
|
||||
percentage = getPercentage()
|
||||
if (warnWhenFull && percentage === 100) {
|
||||
showWarning = true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="usage">
|
||||
<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}
|
||||
<Body size="S">{usage.used}</Body>
|
||||
<Body size="S">{usage.used} / Unlimited</Body>
|
||||
{:else}
|
||||
<Body size="S">{usage.used} / {usage.total}</Body>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
{#if unlimited}
|
||||
<Body size="S">Unlimited</Body>
|
||||
<ProgressBar
|
||||
showPercentage={false}
|
||||
width={"100%"}
|
||||
duration={1}
|
||||
value={100}
|
||||
/>
|
||||
{: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}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -51,6 +89,13 @@
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-m);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.header-container {
|
||||
display: flex;
|
||||
}
|
||||
.heading {
|
||||
margin-top: 3px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
</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)",
|
||||
}
|
||||
|
||||
export const PlanType = {
|
||||
FREE: "free",
|
||||
PRO: "pro",
|
||||
BUSINESS: "business",
|
||||
ENTERPRISE: "enterprise",
|
||||
}
|
||||
|
||||
export const PluginSource = {
|
||||
URL: "URL",
|
||||
NPM: "NPM",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
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 { CookieUtils, Constants } from "@budibase/frontend-core"
|
||||
import { API } from "api"
|
||||
|
@ -63,6 +63,10 @@
|
|||
await auth.getSelf()
|
||||
await admin.init()
|
||||
|
||||
if ($auth.user) {
|
||||
await licensing.init()
|
||||
}
|
||||
|
||||
// Set init info if present
|
||||
if ($params["?template"]) {
|
||||
await auth.setInitInfo({ init_template: $params["?template"] })
|
||||
|
|
|
@ -23,7 +23,8 @@
|
|||
"dataprovider",
|
||||
"repeater",
|
||||
"table",
|
||||
"dynamicfilter"
|
||||
"dynamicfilter",
|
||||
"daterangepicker"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -60,7 +61,6 @@
|
|||
"booleanfield",
|
||||
"longformfield",
|
||||
"attachmentfield",
|
||||
"daterangepicker",
|
||||
"jsonfield",
|
||||
"relationshipfield",
|
||||
"datetimefield",
|
||||
|
@ -80,5 +80,4 @@
|
|||
"donut"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
]
|
|
@ -77,7 +77,7 @@
|
|||
size="L"
|
||||
>
|
||||
<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>
|
||||
<Layout noPadding gap="S">
|
||||
{#each filteredSources as datasource}
|
||||
|
|
|
@ -66,7 +66,7 @@
|
|||
<Heading size="XS">Autogenerated screens</Heading>
|
||||
<Body size="S">
|
||||
Add autogenerated screens with CRUD functionality to get a working
|
||||
app quickly! (Requires a data source)
|
||||
app quickly! (Requires a datasource)
|
||||
</Body>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -13,13 +13,14 @@
|
|||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
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 { AppStatus } from "constants"
|
||||
import { gradient } from "actions"
|
||||
import UpdateUserInfoModal from "components/settings/UpdateUserInfoModal.svelte"
|
||||
import ChangePasswordModal from "components/settings/ChangePasswordModal.svelte"
|
||||
import { processStringSync } from "@budibase/string-templates"
|
||||
import Spaceman from "assets/bb-space-man.svg"
|
||||
import Logo from "assets/bb-emblem.svg"
|
||||
|
||||
let loaded = false
|
||||
|
@ -91,7 +92,7 @@
|
|||
<div class="content">
|
||||
<Layout noPadding>
|
||||
<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">
|
||||
<div slot="control" class="avatar">
|
||||
<Avatar
|
||||
|
@ -131,7 +132,17 @@
|
|||
</Body>
|
||||
</Layout>
|
||||
<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>
|
||||
<div class="group">
|
||||
<Layout gap="S" noPadding>
|
||||
|
@ -194,10 +205,13 @@
|
|||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
img {
|
||||
img.logo {
|
||||
width: 40px;
|
||||
margin-bottom: -12px;
|
||||
}
|
||||
img.spaceman {
|
||||
width: 100px;
|
||||
}
|
||||
.avatar {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
|
|
|
@ -13,8 +13,7 @@
|
|||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import ConfigChecklist from "components/common/ConfigChecklist.svelte"
|
||||
import { organisation, auth } from "stores/portal"
|
||||
import { admin as adminStore } from "stores/portal"
|
||||
import { organisation, auth, admin as adminStore } from "stores/portal"
|
||||
import { onMount } from "svelte"
|
||||
import UpdateUserInfoModal from "components/settings/UpdateUserInfoModal.svelte"
|
||||
import ChangePasswordModal from "components/settings/ChangePasswordModal.svelte"
|
||||
|
@ -37,14 +36,6 @@
|
|||
href: "/builder/portal/apps",
|
||||
},
|
||||
]
|
||||
if (isEnabled(FEATURE_FLAGS.LICENSING)) {
|
||||
menu = menu.concat([
|
||||
{
|
||||
title: "Usage",
|
||||
href: "/builder/portal/settings/usage",
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
if (admin) {
|
||||
menu = menu.concat([
|
||||
|
@ -53,9 +44,20 @@
|
|||
href: "/builder/portal/manage/users",
|
||||
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: "Email", href: "/builder/portal/manage/email" },
|
||||
{ title: "Plugins", href: "/builder/portal/manage/plugins" },
|
||||
{
|
||||
title: "Plugins",
|
||||
href: "/builder/portal/manage/plugins",
|
||||
badge: "New",
|
||||
},
|
||||
|
||||
{
|
||||
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) {
|
||||
menu = menu.concat([
|
||||
{
|
||||
|
@ -84,13 +77,6 @@
|
|||
href: "/builder/portal/settings/update",
|
||||
},
|
||||
])
|
||||
|
||||
if (isEnabled(FEATURE_FLAGS.LICENSING)) {
|
||||
menu = menu.concat({
|
||||
title: "Upgrade",
|
||||
href: "/builder/portal/settings/upgrade",
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
menu = menu.concat([
|
||||
|
@ -103,14 +89,62 @@
|
|||
}
|
||||
|
||||
// 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([
|
||||
{
|
||||
title: "Account",
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -161,11 +195,12 @@
|
|||
</div>
|
||||
<div class="menu">
|
||||
<Navigation>
|
||||
{#each menu as { title, href, heading }}
|
||||
{#each menu as { title, href, heading, badge }}
|
||||
<Item
|
||||
on:click={hideMobileMenu}
|
||||
selected={$isActive(href)}
|
||||
{href}
|
||||
{badge}
|
||||
{heading}>{title}</Item
|
||||
>
|
||||
{/each}
|
||||
|
|
|
@ -13,12 +13,14 @@
|
|||
} from "@budibase/bbui"
|
||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
|
||||
import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte"
|
||||
import { onMount } from "svelte"
|
||||
import { templates } from "stores/portal"
|
||||
import { templates, licensing } from "stores/portal"
|
||||
|
||||
let loaded = $templates?.length
|
||||
let template
|
||||
let creationModal = false
|
||||
let appLimitModal
|
||||
let creatingApp = false
|
||||
|
||||
const welcomeBody =
|
||||
|
@ -29,6 +31,8 @@
|
|||
onMount(async () => {
|
||||
try {
|
||||
await templates.load()
|
||||
await licensing.getQuotaUsage()
|
||||
await licensing.getUsageMetrics()
|
||||
if ($templates?.length === 0) {
|
||||
notifications.error(
|
||||
"There was a problem loading quick start templates."
|
||||
|
@ -41,9 +45,13 @@
|
|||
})
|
||||
|
||||
const initiateAppCreation = () => {
|
||||
template = null
|
||||
creationModal.show()
|
||||
creatingApp = true
|
||||
if ($licensing.usageMetrics.apps >= 100) {
|
||||
appLimitModal.show()
|
||||
} else {
|
||||
template = null
|
||||
creationModal.show()
|
||||
creatingApp = true
|
||||
}
|
||||
}
|
||||
|
||||
const stopAppCreation = () => {
|
||||
|
@ -52,9 +60,13 @@
|
|||
}
|
||||
|
||||
const initiateAppImport = () => {
|
||||
template = { fromFile: true }
|
||||
creationModal.show()
|
||||
creatingApp = true
|
||||
if ($licensing.usageMetrics.apps >= 100) {
|
||||
appLimitModal.show()
|
||||
} else {
|
||||
template = { fromFile: true }
|
||||
creationModal.show()
|
||||
creatingApp = true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -121,6 +133,7 @@
|
|||
>
|
||||
<CreateAppModal {template} />
|
||||
</Modal>
|
||||
<AppLimitModal bind:this={appLimitModal} />
|
||||
|
||||
<style>
|
||||
.title .welcome > .buttons {
|
||||
|
|
|
@ -15,11 +15,12 @@
|
|||
import Spinner from "components/common/Spinner.svelte"
|
||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
||||
import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte"
|
||||
|
||||
import { store, automationStore } from "builderStore"
|
||||
import { API } from "api"
|
||||
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 { goto } from "@roxi/routify"
|
||||
import AppRow from "components/start/AppRow.svelte"
|
||||
|
@ -32,6 +33,7 @@
|
|||
let selectedApp
|
||||
let creationModal
|
||||
let updatingModal
|
||||
let appLimitModal
|
||||
let creatingApp = false
|
||||
let loaded = $apps?.length || $templates?.length
|
||||
let searchTerm = ""
|
||||
|
@ -124,8 +126,10 @@
|
|||
return `${app.name} - Automation error (${errorCount(errors)})`
|
||||
}
|
||||
|
||||
const initiateAppCreation = () => {
|
||||
if ($apps?.length) {
|
||||
const initiateAppCreation = async () => {
|
||||
if ($licensing.usageMetrics.apps >= 100) {
|
||||
appLimitModal.show()
|
||||
} else if ($apps?.length) {
|
||||
$goto("/builder/portal/apps/create")
|
||||
} else {
|
||||
template = null
|
||||
|
@ -225,6 +229,10 @@
|
|||
try {
|
||||
await apps.load()
|
||||
await templates.load()
|
||||
|
||||
await licensing.getQuotaUsage()
|
||||
await licensing.getUsageMetrics()
|
||||
|
||||
if ($templates?.length === 0) {
|
||||
notifications.error(
|
||||
"There was a problem loading quick start templates."
|
||||
|
@ -405,6 +413,8 @@
|
|||
<UpdateAppModal app={selectedApp} />
|
||||
</Modal>
|
||||
|
||||
<AppLimitModal bind:this={appLimitModal} />
|
||||
|
||||
<style>
|
||||
.appTable {
|
||||
border-top: var(--border-light);
|
||||
|
|
|
@ -38,7 +38,11 @@
|
|||
try {
|
||||
await groups.actions.save(group)
|
||||
} 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 || "Beaker"
|
||||
|
||||
$: friendlyName = plugin?.schema?.schema?.friendlyName
|
||||
|
||||
function pluginDeleted() {
|
||||
if (detailsModal) {
|
||||
detailsModal.hide()
|
||||
|
@ -41,6 +43,9 @@
|
|||
>
|
||||
{plugin.name}
|
||||
</Body>
|
||||
<Body size="XS" color="var(--spectrum-global-color-gray-900)">
|
||||
{friendlyName}
|
||||
</Body>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -65,6 +70,11 @@
|
|||
<Input disabled value={plugin.name} />
|
||||
</div>
|
||||
|
||||
<div class="details-row">
|
||||
<Label size="M">Friendly name</Label>
|
||||
<Input disabled value={friendlyName} />
|
||||
</div>
|
||||
|
||||
<div class="details-row">
|
||||
<Label size="M">Type</Label>
|
||||
<Input
|
||||
|
@ -127,7 +137,7 @@
|
|||
|
||||
.details-row {
|
||||
display: grid;
|
||||
grid-template-columns: 60px 1fr;
|
||||
grid-template-columns: 70px 1fr;
|
||||
grid-gap: var(--spacing-l) var(--spacing-l);
|
||||
align-items: center;
|
||||
}
|
||||
|
|
|
@ -88,14 +88,14 @@
|
|||
<Heading size="S">Information</Heading>
|
||||
<Body size="S">Here you can update your logo and organization name.</Body>
|
||||
</Layout>
|
||||
<div className="fields">
|
||||
<div className="field">
|
||||
<div class="fields">
|
||||
<div class="field">
|
||||
<Label size="L">Org. name</Label>
|
||||
<Input thin bind:value={$values.company} />
|
||||
</div>
|
||||
<div className="field logo">
|
||||
<div class="field logo">
|
||||
<Label size="L">Logo</Label>
|
||||
<div className="file">
|
||||
<div class="file">
|
||||
<Dropzone
|
||||
value={[$values.logo]}
|
||||
on:change={e => {
|
||||
|
@ -115,13 +115,14 @@
|
|||
<Heading size="S">Platform</Heading>
|
||||
<Body size="S">Here you can set up general platform settings.</Body>
|
||||
</Layout>
|
||||
<div className="fields">
|
||||
<div className="field">
|
||||
<div class="fields">
|
||||
<div class="field">
|
||||
<Label
|
||||
size="L"
|
||||
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} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -35,10 +35,14 @@
|
|||
}
|
||||
|
||||
const activate = async () => {
|
||||
await API.activateLicenseKey({ licenseKey })
|
||||
await auth.getSelf()
|
||||
await setLicenseInfo()
|
||||
notifications.success("Successfully activated")
|
||||
try {
|
||||
await API.activateLicenseKey({ licenseKey })
|
||||
await auth.getSelf()
|
||||
await setLicenseInfo()
|
||||
notifications.success("Successfully activated")
|
||||
} catch (e) {
|
||||
notifications.error(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
const refresh = async () => {
|
||||
|
|
|
@ -5,49 +5,74 @@
|
|||
Heading,
|
||||
Layout,
|
||||
notifications,
|
||||
Detail,
|
||||
Link,
|
||||
TooltipWrapper,
|
||||
} from "@budibase/bbui"
|
||||
import { onMount } from "svelte"
|
||||
import { admin, auth, licensing } from "stores/portal"
|
||||
import Usage from "components/usage/Usage.svelte"
|
||||
import { admin, auth, licensing } from "../../../../stores/portal"
|
||||
import { PlanType } from "../../../../constants"
|
||||
import { DashCard, Usage } from "../../../../components/usage"
|
||||
|
||||
let staticUsage = []
|
||||
let monthlyUsage = []
|
||||
let cancelAt
|
||||
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
|
||||
$: license = $auth.user?.license
|
||||
|
||||
const upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade`
|
||||
$: accountPortalAccess = $auth?.user?.accountPortalAccess
|
||||
$: quotaReset = quotaUsage?.quotaReset
|
||||
|
||||
const setMonthlyUsage = () => {
|
||||
monthlyUsage = []
|
||||
if (quotaUsage.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]
|
||||
if (used !== undefined) {
|
||||
if (value.value !== 0) {
|
||||
monthlyUsage.push({
|
||||
name: value.name,
|
||||
used: used,
|
||||
used: used ? used : 0,
|
||||
total: value.value,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
monthlyUsage = monthlyUsage.sort((a, b) => a.name.localeCompare(b.name))
|
||||
}
|
||||
|
||||
const setStaticUsage = () => {
|
||||
staticUsage = []
|
||||
for (let [key, value] of Object.entries(license.quotas.usage.static)) {
|
||||
if (EXCLUDE_QUOTAS.includes(value.name)) {
|
||||
continue
|
||||
}
|
||||
const used = quotaUsage.usageQuota[key]
|
||||
if (used !== undefined) {
|
||||
if (value.value !== 0) {
|
||||
staticUsage.push({
|
||||
name: value.name,
|
||||
used: used,
|
||||
used: used ? used : 0,
|
||||
total: value.value,
|
||||
})
|
||||
}
|
||||
}
|
||||
staticUsage = staticUsage.sort((a, b) => a.name.localeCompare(b.name))
|
||||
}
|
||||
|
||||
const setCancelAt = () => {
|
||||
cancelAt = license?.billing?.subscription?.cancelAt
|
||||
}
|
||||
|
||||
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 () => {
|
||||
try {
|
||||
await licensing.getQuotaUsage()
|
||||
|
@ -71,69 +160,98 @@
|
|||
})
|
||||
|
||||
$: {
|
||||
if (license && quotaUsage) {
|
||||
setMonthlyUsage()
|
||||
setStaticUsage()
|
||||
if (license) {
|
||||
setPrimaryActionText()
|
||||
setCancelAt()
|
||||
setTextRows()
|
||||
setDaysRemainingInMonth()
|
||||
|
||||
if (quotaUsage) {
|
||||
setMonthlyUsage()
|
||||
setStaticUsage()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if loaded}
|
||||
<Layout>
|
||||
<Heading>Usage</Heading>
|
||||
<Body
|
||||
>Get information about your current usage within Budibase.
|
||||
{#if $admin.cloud}
|
||||
{#if $auth.user?.accountPortalAccess}
|
||||
<Layout noPadding gap="S">
|
||||
<Heading>Usage</Heading>
|
||||
<Body
|
||||
>Get information about your current usage within Budibase.
|
||||
{#if accountPortalAccess}
|
||||
To upgrade your plan and usage limits visit your <Link
|
||||
size="L"
|
||||
href={upgradeUrl}>Account</Link
|
||||
>.
|
||||
on:click={goToAccountPortal}
|
||||
size="L">Account</Link
|
||||
>
|
||||
{:else}
|
||||
Contact your account holder to upgrade your usage limits.
|
||||
To upgrade your plan and usage limits contact your account holder
|
||||
{/if}
|
||||
{/if}
|
||||
</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>
|
||||
</Body>
|
||||
</Layout>
|
||||
<Layout gap="S">
|
||||
<Body size="S">USAGE</Body>
|
||||
<div class="usages">
|
||||
{#each staticUsage as usage}
|
||||
<div class="usage">
|
||||
<Usage {usage} />
|
||||
<Divider />
|
||||
<DashCard
|
||||
description="YOUR CURRENT PLAN"
|
||||
title={planTitle()}
|
||||
{primaryActionText}
|
||||
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>
|
||||
{/each}
|
||||
</div>
|
||||
</Layout>
|
||||
{#if monthlyUsage.length}
|
||||
<Layout gap="S">
|
||||
<Body size="S">MONTHLY</Body>
|
||||
<div class="usages">
|
||||
{#each monthlyUsage as usage}
|
||||
<div class="usage">
|
||||
<Usage {usage} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Layout>
|
||||
{#if monthlyUsage.length}
|
||||
<div class="monthly-container">
|
||||
<Layout gap="S">
|
||||
<Heading size="S" weight="light">Monthly</Heading>
|
||||
<div class="detail">
|
||||
<TooltipWrapper tooltip={new Date(quotaReset)}>
|
||||
<Detail size="M">Resets in {daysRemainingInMonth} days</Detail
|
||||
>
|
||||
</TooltipWrapper>
|
||||
</div>
|
||||
<div class="usages">
|
||||
<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>
|
||||
<div />
|
||||
{/if}
|
||||
</DashCard>
|
||||
</Layout>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.usages {
|
||||
display: grid;
|
||||
column-gap: 60px;
|
||||
row-gap: 50px;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.detail :global(.spectrum-Detail) {
|
||||
color: var(--spectrum-global-color-gray-700);
|
||||
}
|
||||
.detail :global(.icon) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,14 +1,22 @@
|
|||
import { writable } from "svelte/store"
|
||||
import { writable, get } from "svelte/store"
|
||||
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 = () => {
|
||||
const DEFAULT = {
|
||||
plans: {},
|
||||
}
|
||||
const oneDayInMilliseconds = 86400000
|
||||
|
||||
const store = writable(DEFAULT)
|
||||
|
||||
const actions = {
|
||||
init: async () => {
|
||||
await actions.getQuotaUsage()
|
||||
await actions.getUsageMetrics()
|
||||
},
|
||||
getQuotaUsage: async () => {
|
||||
const quotaUsage = await API.getQuotaUsage()
|
||||
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 {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/cli",
|
||||
"version": "1.3.15-alpha.7",
|
||||
"version": "1.3.19-alpha.6",
|
||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
|
@ -26,9 +26,9 @@
|
|||
"outputPath": "build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/backend-core": "1.3.15-alpha.7",
|
||||
"@budibase/string-templates": "1.3.15-alpha.7",
|
||||
"@budibase/types": "1.3.15-alpha.7",
|
||||
"@budibase/backend-core": "1.3.19-alpha.6",
|
||||
"@budibase/string-templates": "1.3.19-alpha.6",
|
||||
"@budibase/types": "1.3.19-alpha.6",
|
||||
"axios": "0.21.2",
|
||||
"chalk": "4.1.0",
|
||||
"cli-progress": "3.11.2",
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
const { Event } = require("@budibase/types")
|
||||
|
||||
exports.CommandWords = {
|
||||
BACKUPS: "backups",
|
||||
HOSTING: "hosting",
|
||||
|
@ -15,6 +17,7 @@ exports.AnalyticsEvents = {
|
|||
OptOut: "analytics:opt:out",
|
||||
OptIn: "analytics:opt:in",
|
||||
SelfHostInit: "hosting:init",
|
||||
PluginInit: Event.PLUGIN_INIT,
|
||||
}
|
||||
|
||||
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 makeEnv = require("./makeEnv")
|
||||
const axios = require("axios")
|
||||
const AnalyticsClient = require("../analytics/Client")
|
||||
const { captureEvent } = require("../events")
|
||||
|
||||
const BUDIBASE_SERVICES = ["app-service", "worker-service", "proxy-service"]
|
||||
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 client = new AnalyticsClient()
|
||||
|
||||
async function downloadFiles() {
|
||||
const promises = []
|
||||
for (let url of FILE_URLS) {
|
||||
|
@ -72,12 +70,8 @@ async function init(type) {
|
|||
return
|
||||
}
|
||||
}
|
||||
client.capture({
|
||||
distinctId: "cli",
|
||||
event: AnalyticsEvents.SelfHostInit,
|
||||
properties: {
|
||||
type,
|
||||
},
|
||||
captureEvent(AnalyticsEvents.SelfHostInit, {
|
||||
type,
|
||||
})
|
||||
await downloadFiles()
|
||||
const config = isQuick ? makeEnv.QUICK_CONFIG : {}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
const Command = require("../structures/Command")
|
||||
const { CommandWords } = require("../constants")
|
||||
const { CommandWords, AnalyticsEvents } = require("../constants")
|
||||
const { getSkeleton, fleshOutSkeleton } = require("./skeleton")
|
||||
const questions = require("../questions")
|
||||
const fs = require("fs")
|
||||
|
@ -8,6 +8,7 @@ const { validate } = require("@budibase/backend-core/plugins")
|
|||
const { runPkgCommand } = require("../exec")
|
||||
const { join } = require("path")
|
||||
const { success, error, info, moveDirectory } = require("../utils")
|
||||
const { captureEvent } = require("../events")
|
||||
|
||||
function checkInPlugin() {
|
||||
if (!fs.existsSync("package.json")) {
|
||||
|
@ -58,7 +59,7 @@ async function init(opts) {
|
|||
)
|
||||
return
|
||||
}
|
||||
const desc = await questions.string(
|
||||
const description = await questions.string(
|
||||
"Description",
|
||||
`An amazing Budibase ${type}!`
|
||||
)
|
||||
|
@ -67,7 +68,7 @@ async function init(opts) {
|
|||
// get the skeleton
|
||||
console.log(info("Retrieving project..."))
|
||||
await getSkeleton(type, name)
|
||||
await fleshOutSkeleton(type, name, desc, version)
|
||||
await fleshOutSkeleton(type, name, description, version)
|
||||
console.log(info("Installing dependencies..."))
|
||||
await runPkgCommand("install", join(process.cwd(), name))
|
||||
// if no parent directory desired move to cwd
|
||||
|
@ -77,6 +78,12 @@ async function init(opts) {
|
|||
} else {
|
||||
console.log(info(`Plugin created in directory "${name}"`))
|
||||
}
|
||||
captureEvent(AnalyticsEvents.PluginInit, {
|
||||
type,
|
||||
name,
|
||||
description,
|
||||
version,
|
||||
})
|
||||
}
|
||||
|
||||
async function verify() {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/client",
|
||||
"version": "1.3.15-alpha.7",
|
||||
"version": "1.3.19-alpha.6",
|
||||
"license": "MPL-2.0",
|
||||
"module": "dist/budibase-client.js",
|
||||
"main": "dist/budibase-client.js",
|
||||
|
@ -19,9 +19,9 @@
|
|||
"dev:builder": "rollup -cw"
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "1.3.15-alpha.7",
|
||||
"@budibase/frontend-core": "1.3.15-alpha.7",
|
||||
"@budibase/string-templates": "1.3.15-alpha.7",
|
||||
"@budibase/bbui": "1.3.19-alpha.6",
|
||||
"@budibase/frontend-core": "1.3.19-alpha.6",
|
||||
"@budibase/string-templates": "1.3.19-alpha.6",
|
||||
"@spectrum-css/button": "^3.0.3",
|
||||
"@spectrum-css/card": "^3.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"
|
||||
target="_blank"
|
||||
>
|
||||
<div>
|
||||
<img src="https://i.imgur.com/Xhdt1YP.png" alt="Budibase" />
|
||||
<p>Made In Budibase</p>
|
||||
<p>Made with Budibase</p>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<style>
|
||||
div {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
padding: 10px;
|
||||
|
@ -27,12 +32,7 @@
|
|||
|
||||
p {
|
||||
text-decoration: none;
|
||||
color: var(--spectrum-heading-m-text-color);
|
||||
}
|
||||
|
||||
a:visited {
|
||||
text-decoration: none;
|
||||
color: var(--spectrum-heading-m-text-color);
|
||||
color: var(--spectrum-global-color-gray-900);
|
||||
}
|
||||
|
||||
img {
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue