Merge pull request #7782 from Budibase/feature/day-pass-pricing
Day pass pricing
This commit is contained in:
commit
060251ab03
|
@ -7,6 +7,7 @@ exports.Cookies = {
|
||||||
CurrentApp: "budibase:currentapp",
|
CurrentApp: "budibase:currentapp",
|
||||||
Auth: "budibase:auth",
|
Auth: "budibase:auth",
|
||||||
Init: "budibase:init",
|
Init: "budibase:init",
|
||||||
|
ACCOUNT_RETURN_URL: "budibase:account:returnurl",
|
||||||
DatasourceAuth: "budibase:datasourceauth",
|
DatasourceAuth: "budibase:datasourceauth",
|
||||||
OIDC_CONFIG: "budibase:oidc:config",
|
OIDC_CONFIG: "budibase:oidc:config",
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { DocumentType, ViewName, DeprecatedViews, SEPARATOR } from "./utils"
|
import { DocumentType, ViewName, DeprecatedViews, SEPARATOR } from "./utils"
|
||||||
import { getGlobalDB } from "../context"
|
import { getGlobalDB } from "../context"
|
||||||
|
import PouchDB from "pouchdb"
|
||||||
import { StaticDatabases } from "./constants"
|
import { StaticDatabases } from "./constants"
|
||||||
import { doWithDB } from "./"
|
import { doWithDB } from "./"
|
||||||
|
|
||||||
|
@ -201,13 +202,13 @@ export const queryView = async <T>(
|
||||||
try {
|
try {
|
||||||
let response = await db.query<T, T>(`database/${viewName}`, params)
|
let response = await db.query<T, T>(`database/${viewName}`, params)
|
||||||
const rows = response.rows
|
const rows = response.rows
|
||||||
const docs = rows.map((resp: any) =>
|
const docs = rows.map(row => (params.include_docs ? row.doc : row.value))
|
||||||
params.include_docs ? resp.doc : resp.value
|
|
||||||
)
|
|
||||||
|
|
||||||
|
// if arrayResponse has been requested, always return array regardless of length
|
||||||
if (opts?.arrayResponse) {
|
if (opts?.arrayResponse) {
|
||||||
return docs
|
return docs
|
||||||
} else {
|
} else {
|
||||||
|
// return the single document if there is only one
|
||||||
return docs.length <= 1 ? docs[0] : docs
|
return docs.length <= 1 ? docs[0] : docs
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|
|
@ -37,7 +37,7 @@ const env = {
|
||||||
MULTI_TENANCY: process.env.MULTI_TENANCY,
|
MULTI_TENANCY: process.env.MULTI_TENANCY,
|
||||||
ACCOUNT_PORTAL_URL:
|
ACCOUNT_PORTAL_URL:
|
||||||
process.env.ACCOUNT_PORTAL_URL || "https://account.budibase.app",
|
process.env.ACCOUNT_PORTAL_URL || "https://account.budibase.app",
|
||||||
ACCOUNT_PORTAL_API_KEY: process.env.ACCOUNT_PORTAL_API_KEY,
|
ACCOUNT_PORTAL_API_KEY: process.env.ACCOUNT_PORTAL_API_KEY || "",
|
||||||
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
|
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
|
||||||
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED || ""),
|
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED || ""),
|
||||||
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
|
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
class BudibaseError extends Error {
|
|
||||||
constructor(message, code, type) {
|
|
||||||
super(message)
|
|
||||||
this.code = code
|
|
||||||
this.type = type
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
BudibaseError,
|
|
||||||
}
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
export class BudibaseError extends Error {
|
||||||
|
code: string
|
||||||
|
type: string
|
||||||
|
|
||||||
|
constructor(message: string, code: string, type: string) {
|
||||||
|
super(message)
|
||||||
|
this.code = code
|
||||||
|
this.type = type
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +0,0 @@
|
||||||
const { BudibaseError } = require("./base")
|
|
||||||
|
|
||||||
class GenericError extends BudibaseError {
|
|
||||||
constructor(message, code, type) {
|
|
||||||
super(message, code, type ? type : "generic")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
GenericError,
|
|
||||||
}
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { BudibaseError } from "./base"
|
||||||
|
|
||||||
|
export class GenericError extends BudibaseError {
|
||||||
|
constructor(message: string, code: string, type: string) {
|
||||||
|
super(message, code, type ? type : "generic")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +0,0 @@
|
||||||
const { GenericError } = require("./generic")
|
|
||||||
|
|
||||||
class HTTPError extends GenericError {
|
|
||||||
constructor(message, httpStatus, code = "http", type = "generic") {
|
|
||||||
super(message, code, type)
|
|
||||||
this.status = httpStatus
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
HTTPError,
|
|
||||||
}
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { GenericError } from "./generic"
|
||||||
|
|
||||||
|
export class HTTPError extends GenericError {
|
||||||
|
status: number
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
httpStatus: number,
|
||||||
|
code = "http",
|
||||||
|
type = "generic"
|
||||||
|
) {
|
||||||
|
super(message, code, type)
|
||||||
|
this.status = httpStatus
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
const http = require("./http")
|
import { HTTPError } from "./http"
|
||||||
const licensing = require("./licensing")
|
import { UsageLimitError, FeatureDisabledError } from "./licensing"
|
||||||
|
import * as licensing from "./licensing"
|
||||||
|
|
||||||
const codes = {
|
const codes = {
|
||||||
...licensing.codes,
|
...licensing.codes,
|
||||||
|
@ -11,7 +12,7 @@ const context = {
|
||||||
...licensing.context,
|
...licensing.context,
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPublicError = err => {
|
const getPublicError = (err: any) => {
|
||||||
let error
|
let error
|
||||||
if (err.code || err.type) {
|
if (err.code || err.type) {
|
||||||
// add generic error information
|
// add generic error information
|
||||||
|
@ -32,13 +33,15 @@ const getPublicError = err => {
|
||||||
return error
|
return error
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
const pkg = {
|
||||||
codes,
|
codes,
|
||||||
types,
|
types,
|
||||||
errors: {
|
errors: {
|
||||||
UsageLimitError: licensing.UsageLimitError,
|
UsageLimitError,
|
||||||
FeatureDisabledError: licensing.FeatureDisabledError,
|
FeatureDisabledError,
|
||||||
HTTPError: http.HTTPError,
|
HTTPError,
|
||||||
},
|
},
|
||||||
getPublicError,
|
getPublicError,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export = pkg
|
|
@ -1,43 +0,0 @@
|
||||||
const { HTTPError } = require("./http")
|
|
||||||
|
|
||||||
const type = "license_error"
|
|
||||||
|
|
||||||
const codes = {
|
|
||||||
USAGE_LIMIT_EXCEEDED: "usage_limit_exceeded",
|
|
||||||
FEATURE_DISABLED: "feature_disabled",
|
|
||||||
}
|
|
||||||
|
|
||||||
const context = {
|
|
||||||
[codes.USAGE_LIMIT_EXCEEDED]: err => {
|
|
||||||
return {
|
|
||||||
limitName: err.limitName,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[codes.FEATURE_DISABLED]: err => {
|
|
||||||
return {
|
|
||||||
featureName: err.featureName,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
class UsageLimitError extends HTTPError {
|
|
||||||
constructor(message, limitName) {
|
|
||||||
super(message, 400, codes.USAGE_LIMIT_EXCEEDED, type)
|
|
||||||
this.limitName = limitName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FeatureDisabledError extends HTTPError {
|
|
||||||
constructor(message, featureName) {
|
|
||||||
super(message, 400, codes.FEATURE_DISABLED, type)
|
|
||||||
this.featureName = featureName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
type,
|
|
||||||
codes,
|
|
||||||
context,
|
|
||||||
UsageLimitError,
|
|
||||||
FeatureDisabledError,
|
|
||||||
}
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { HTTPError } from "./http"
|
||||||
|
|
||||||
|
export const type = "license_error"
|
||||||
|
|
||||||
|
export const codes = {
|
||||||
|
USAGE_LIMIT_EXCEEDED: "usage_limit_exceeded",
|
||||||
|
FEATURE_DISABLED: "feature_disabled",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const context = {
|
||||||
|
[codes.USAGE_LIMIT_EXCEEDED]: (err: any) => {
|
||||||
|
return {
|
||||||
|
limitName: err.limitName,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[codes.FEATURE_DISABLED]: (err: any) => {
|
||||||
|
return {
|
||||||
|
featureName: err.featureName,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UsageLimitError extends HTTPError {
|
||||||
|
limitName: string
|
||||||
|
|
||||||
|
constructor(message: string, limitName: string) {
|
||||||
|
super(message, 400, codes.USAGE_LIMIT_EXCEEDED, type)
|
||||||
|
this.limitName = limitName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FeatureDisabledError extends HTTPError {
|
||||||
|
featureName: string
|
||||||
|
|
||||||
|
constructor(message: string, featureName: string) {
|
||||||
|
super(message, 400, codes.FEATURE_DISABLED, type)
|
||||||
|
this.featureName = featureName
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,20 +31,26 @@ const TENANT_FEATURE_FLAGS = getFeatureFlags()
|
||||||
|
|
||||||
exports.isEnabled = featureFlag => {
|
exports.isEnabled = featureFlag => {
|
||||||
const tenantId = tenancy.getTenantId()
|
const tenantId = tenancy.getTenantId()
|
||||||
|
const flags = exports.getTenantFeatureFlags(tenantId)
|
||||||
return (
|
return flags.includes(featureFlag)
|
||||||
TENANT_FEATURE_FLAGS &&
|
|
||||||
TENANT_FEATURE_FLAGS[tenantId] &&
|
|
||||||
TENANT_FEATURE_FLAGS[tenantId].includes(featureFlag)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getTenantFeatureFlags = tenantId => {
|
exports.getTenantFeatureFlags = tenantId => {
|
||||||
if (TENANT_FEATURE_FLAGS && TENANT_FEATURE_FLAGS[tenantId]) {
|
const flags = []
|
||||||
return TENANT_FEATURE_FLAGS[tenantId]
|
|
||||||
|
if (TENANT_FEATURE_FLAGS) {
|
||||||
|
const globalFlags = TENANT_FEATURE_FLAGS["*"]
|
||||||
|
const tenantFlags = TENANT_FEATURE_FLAGS[tenantId]
|
||||||
|
|
||||||
|
if (globalFlags) {
|
||||||
|
flags.push(...globalFlags)
|
||||||
|
}
|
||||||
|
if (tenantFlags) {
|
||||||
|
flags.push(...tenantFlags)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return []
|
return flags
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.FeatureFlag = {
|
exports.FeatureFlag = {
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import errors from "./errors"
|
import errors from "./errors"
|
||||||
|
|
||||||
const errorClasses = errors.errors
|
const errorClasses = errors.errors
|
||||||
import * as events from "./events"
|
import * as events from "./events"
|
||||||
import * as migrations from "./migrations"
|
import * as migrations from "./migrations"
|
||||||
|
@ -15,7 +14,7 @@ import deprovisioning from "./context/deprovision"
|
||||||
import auth from "./auth"
|
import auth from "./auth"
|
||||||
import constants from "./constants"
|
import constants from "./constants"
|
||||||
import * as dbConstants from "./db/constants"
|
import * as dbConstants from "./db/constants"
|
||||||
import logging from "./logging"
|
import * as logging from "./logging"
|
||||||
import pino from "./pino"
|
import pino from "./pino"
|
||||||
import * as middleware from "./middleware"
|
import * as middleware from "./middleware"
|
||||||
import plugins from "./plugin"
|
import plugins from "./plugin"
|
||||||
|
|
|
@ -106,6 +106,7 @@ export = (
|
||||||
user = await getUser(userId, session.tenantId)
|
user = await getUser(userId, session.tenantId)
|
||||||
}
|
}
|
||||||
user.csrfToken = session.csrfToken
|
user.csrfToken = session.csrfToken
|
||||||
|
|
||||||
if (session?.lastAccessedAt < timeMinusOneMinute()) {
|
if (session?.lastAccessedAt < timeMinusOneMinute()) {
|
||||||
// make sure we denote that the session is still in use
|
// make sure we denote that the session is still in use
|
||||||
await updateSessionTTL(session)
|
await updateSessionTTL(session)
|
||||||
|
|
|
@ -17,14 +17,6 @@ export const DEFINITIONS: MigrationDefinition[] = [
|
||||||
type: MigrationType.APP,
|
type: MigrationType.APP,
|
||||||
name: MigrationName.APP_URLS,
|
name: MigrationName.APP_URLS,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
type: MigrationType.GLOBAL,
|
|
||||||
name: MigrationName.DEVELOPER_QUOTA,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: MigrationType.GLOBAL,
|
|
||||||
name: MigrationName.PUBLISHED_APP_QUOTA,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: MigrationType.APP,
|
type: MigrationType.APP,
|
||||||
name: MigrationName.EVENT_APP_BACKFILL,
|
name: MigrationName.EVENT_APP_BACKFILL,
|
||||||
|
|
|
@ -2,28 +2,12 @@ const redis = require("../redis/init")
|
||||||
const { v4: uuidv4 } = require("uuid")
|
const { v4: uuidv4 } = require("uuid")
|
||||||
const { logWarn } = require("../logging")
|
const { logWarn } = require("../logging")
|
||||||
const env = require("../environment")
|
const env = require("../environment")
|
||||||
|
import {
|
||||||
interface CreateSession {
|
Session,
|
||||||
sessionId: string
|
ScannedSession,
|
||||||
tenantId: string
|
SessionKey,
|
||||||
csrfToken?: string
|
CreateSession,
|
||||||
}
|
} from "@budibase/types"
|
||||||
|
|
||||||
interface Session extends CreateSession {
|
|
||||||
userId: string
|
|
||||||
lastAccessedAt: string
|
|
||||||
createdAt: string
|
|
||||||
// make optional attributes required
|
|
||||||
csrfToken: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SessionKey {
|
|
||||||
key: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ScannedSession {
|
|
||||||
value: Session
|
|
||||||
}
|
|
||||||
|
|
||||||
// a week in seconds
|
// a week in seconds
|
||||||
const EXPIRY_SECONDS = 86400 * 7
|
const EXPIRY_SECONDS = 86400 * 7
|
||||||
|
|
|
@ -1,29 +1,39 @@
|
||||||
const {
|
import {
|
||||||
ViewName,
|
ViewName,
|
||||||
getUsersByAppParams,
|
getUsersByAppParams,
|
||||||
getProdAppID,
|
getProdAppID,
|
||||||
generateAppUserID,
|
generateAppUserID,
|
||||||
} = require("./db/utils")
|
} from "./db/utils"
|
||||||
const { queryGlobalView } = require("./db/views")
|
import { queryGlobalView } from "./db/views"
|
||||||
const { UNICODE_MAX } = require("./db/constants")
|
import { UNICODE_MAX } from "./db/constants"
|
||||||
|
import { User } from "@budibase/types"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given an email address this will use a view to search through
|
* Given an email address this will use a view to search through
|
||||||
* all the users to find one with this email address.
|
* all the users to find one with this email address.
|
||||||
* @param {string} email the email to lookup the user by.
|
* @param {string} email the email to lookup the user by.
|
||||||
*/
|
*/
|
||||||
exports.getGlobalUserByEmail = async email => {
|
export const getGlobalUserByEmail = async (
|
||||||
|
email: String
|
||||||
|
): Promise<User | undefined> => {
|
||||||
if (email == null) {
|
if (email == null) {
|
||||||
throw "Must supply an email address to view"
|
throw "Must supply an email address to view"
|
||||||
}
|
}
|
||||||
|
|
||||||
return await queryGlobalView(ViewName.USER_BY_EMAIL, {
|
const response = await queryGlobalView<User>(ViewName.USER_BY_EMAIL, {
|
||||||
key: email.toLowerCase(),
|
key: email.toLowerCase(),
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (Array.isArray(response)) {
|
||||||
|
// shouldn't be able to happen, but need to handle just in case
|
||||||
|
throw new Error(`Multiple users found with email address: ${email}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.searchGlobalUsersByApp = async (appId, opts) => {
|
export const searchGlobalUsersByApp = async (appId: any, opts: any) => {
|
||||||
if (typeof appId !== "string") {
|
if (typeof appId !== "string") {
|
||||||
throw new Error("Must provide a string based app ID")
|
throw new Error("Must provide a string based app ID")
|
||||||
}
|
}
|
||||||
|
@ -38,24 +48,24 @@ exports.searchGlobalUsersByApp = async (appId, opts) => {
|
||||||
return Array.isArray(response) ? response : [response]
|
return Array.isArray(response) ? response : [response]
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getGlobalUserByAppPage = (appId, user) => {
|
export const getGlobalUserByAppPage = (appId: string, user: User) => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return generateAppUserID(getProdAppID(appId), user._id)
|
return generateAppUserID(getProdAppID(appId), user._id!)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performs a starts with search on the global email view.
|
* Performs a starts with search on the global email view.
|
||||||
*/
|
*/
|
||||||
exports.searchGlobalUsersByEmail = async (email, opts) => {
|
export const searchGlobalUsersByEmail = async (email: string, opts: any) => {
|
||||||
if (typeof email !== "string") {
|
if (typeof email !== "string") {
|
||||||
throw new Error("Must provide a string to search by")
|
throw new Error("Must provide a string to search by")
|
||||||
}
|
}
|
||||||
const lcEmail = email.toLowerCase()
|
const lcEmail = email.toLowerCase()
|
||||||
// handle if passing up startkey for pagination
|
// handle if passing up startkey for pagination
|
||||||
const startkey = opts && opts.startkey ? opts.startkey : lcEmail
|
const startkey = opts && opts.startkey ? opts.startkey : lcEmail
|
||||||
let response = await queryGlobalView(ViewName.USER_BY_EMAIL, {
|
let response = await queryGlobalView<User>(ViewName.USER_BY_EMAIL, {
|
||||||
...opts,
|
...opts,
|
||||||
startkey,
|
startkey,
|
||||||
endkey: `${lcEmail}${UNICODE_MAX}`,
|
endkey: `${lcEmail}${UNICODE_MAX}`,
|
|
@ -42,6 +42,18 @@ async function resolveAppUrl(ctx) {
|
||||||
return app && app.appId ? app.appId : undefined
|
return app && app.appId ? app.appId : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.isServingApp = ctx => {
|
||||||
|
// dev app
|
||||||
|
if (ctx.path.startsWith(`/${APP_PREFIX}`)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// prod app
|
||||||
|
if (ctx.path.startsWith(PROD_APP_PREFIX)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a request tries to find the appId, which can be located in various places
|
* Given a request tries to find the appId, which can be located in various places
|
||||||
* @param {object} ctx The main request body to look through.
|
* @param {object} ctx The main request body to look through.
|
||||||
|
|
|
@ -4,22 +4,32 @@
|
||||||
import { banner } from "../Stores/banner"
|
import { banner } from "../Stores/banner"
|
||||||
import Banner from "./Banner.svelte"
|
import Banner from "./Banner.svelte"
|
||||||
import { fly } from "svelte/transition"
|
import { fly } from "svelte/transition"
|
||||||
|
import TooltipWrapper from "../Tooltip/TooltipWrapper.svelte"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Portal target=".banner-container">
|
<Portal target=".banner-container">
|
||||||
<div class="banner">
|
<div class="banner">
|
||||||
{#if $banner.message}
|
{#each $banner.messages as message}
|
||||||
<div transition:fly={{ y: -30 }}>
|
<div transition:fly={{ y: -30 }}>
|
||||||
<Banner
|
<Banner
|
||||||
type={$banner.type}
|
type={message.type}
|
||||||
extraButtonText={$banner.extraButtonText}
|
extraButtonText={message.extraButtonText}
|
||||||
extraButtonAction={$banner.extraButtonAction}
|
extraButtonAction={message.extraButtonAction}
|
||||||
on:change={$banner.onChange}
|
on:change={() => {
|
||||||
|
if (message.onChange) {
|
||||||
|
message.onChange()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
showCloseButton={typeof message.showCloseButton === "boolean"
|
||||||
|
? message.showCloseButton
|
||||||
|
: true}
|
||||||
>
|
>
|
||||||
{$banner.message}
|
<TooltipWrapper tooltip={message.tooltip} disabled={false}>
|
||||||
|
{message.message}
|
||||||
|
</TooltipWrapper>
|
||||||
</Banner>
|
</Banner>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</Portal>
|
</Portal>
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
export let header = ""
|
export let header = ""
|
||||||
export let message = ""
|
export let message = ""
|
||||||
export let onConfirm = undefined
|
export let onConfirm = undefined
|
||||||
|
export let buttonText = ""
|
||||||
|
|
||||||
$: icon = selectIcon(type)
|
$: icon = selectIcon(type)
|
||||||
// if newlines used, convert them to different elements
|
// if newlines used, convert them to different elements
|
||||||
|
@ -39,13 +40,16 @@
|
||||||
<div class="spectrum-InLineAlert-content">{splitMsg}</div>
|
<div class="spectrum-InLineAlert-content">{splitMsg}</div>
|
||||||
{/each}
|
{/each}
|
||||||
{#if onConfirm}
|
{#if onConfirm}
|
||||||
<div class="spectrum-InLineAlert-footer">
|
<div class="spectrum-InLineAlert-footer button">
|
||||||
<Button secondary on:click={onConfirm}>OK</Button>
|
<Button secondary on:click={onConfirm}>{buttonText || "OK"}</Button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.button {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
.spectrum-InLineAlert {
|
.spectrum-InLineAlert {
|
||||||
--spectrum-semantic-negative-border-color: #e34850;
|
--spectrum-semantic-negative-border-color: #e34850;
|
||||||
--spectrum-semantic-positive-border-color: #2d9d78;
|
--spectrum-semantic-positive-border-color: #2d9d78;
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
export let duration = 1000
|
export let duration = 1000
|
||||||
export let width = false
|
export let width = false
|
||||||
export let sideLabel = false
|
export let sideLabel = false
|
||||||
|
export let hidePercentage = true
|
||||||
|
export let color // red, green, default = blue
|
||||||
|
|
||||||
export let size = "M"
|
export let size = "M"
|
||||||
|
|
||||||
|
@ -37,7 +39,7 @@
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if value || value === 0}
|
{#if !hidePercentage && (value || value === 0)}
|
||||||
<div
|
<div
|
||||||
class="spectrum-FieldLabel spectrum-ProgressBar-percentage spectrum-FieldLabel--size{size}"
|
class="spectrum-FieldLabel spectrum-ProgressBar-percentage spectrum-FieldLabel--size{size}"
|
||||||
>
|
>
|
||||||
|
@ -47,8 +49,19 @@
|
||||||
<div class="spectrum-ProgressBar-track">
|
<div class="spectrum-ProgressBar-track">
|
||||||
<div
|
<div
|
||||||
class="spectrum-ProgressBar-fill"
|
class="spectrum-ProgressBar-fill"
|
||||||
|
class:color-green={color === "green"}
|
||||||
|
class:color-red={color === "red"}
|
||||||
style={value || value === 0 ? `width: ${$progress}%` : ""}
|
style={value || value === 0 ? `width: ${$progress}%` : ""}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="spectrum-ProgressBar-label" hidden="" />
|
<div class="spectrum-ProgressBar-label" hidden="" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.color-green {
|
||||||
|
background: #009562;
|
||||||
|
}
|
||||||
|
.color-red {
|
||||||
|
background: #dd2019;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
const multilevel = getContext("sidenav-type")
|
const multilevel = getContext("sidenav-type")
|
||||||
|
import Badge from "../Badge/Badge.svelte"
|
||||||
export let href = ""
|
export let href = ""
|
||||||
export let external = false
|
export let external = false
|
||||||
export let heading = ""
|
export let heading = ""
|
||||||
|
@ -8,6 +9,7 @@
|
||||||
export let selected = false
|
export let selected = false
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let dataCy
|
export let dataCy
|
||||||
|
export let badge = ""
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<li
|
<li
|
||||||
|
@ -38,10 +40,22 @@
|
||||||
</svg>
|
</svg>
|
||||||
{/if}
|
{/if}
|
||||||
<slot />
|
<slot />
|
||||||
|
{#if badge}
|
||||||
|
<div class="badge">
|
||||||
|
<Badge active size="S">{badge}</Badge>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{#if multilevel && $$slots.subnav}
|
{#if multilevel && $$slots.subnav}
|
||||||
<ul class="spectrum-SideNav">
|
<ul class="spectrum-SideNav">
|
||||||
<slot name="subnav" />
|
<slot name="subnav" />
|
||||||
</ul>
|
</ul>
|
||||||
{/if}
|
{/if}
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.badge {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
import { writable } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
|
|
||||||
|
export const BANNER_TYPES = {
|
||||||
|
INFO: "info",
|
||||||
|
NEGATIVE: "negative",
|
||||||
|
}
|
||||||
|
|
||||||
export function createBannerStore() {
|
export function createBannerStore() {
|
||||||
const DEFAULT_CONFIG = {}
|
const DEFAULT_CONFIG = {
|
||||||
|
messages: [],
|
||||||
|
}
|
||||||
|
|
||||||
const banner = writable(DEFAULT_CONFIG)
|
const banner = writable(DEFAULT_CONFIG)
|
||||||
|
|
||||||
|
@ -20,17 +27,38 @@ export function createBannerStore() {
|
||||||
const showStatus = async () => {
|
const showStatus = async () => {
|
||||||
const config = {
|
const config = {
|
||||||
message: "Some systems are experiencing issues",
|
message: "Some systems are experiencing issues",
|
||||||
type: "negative",
|
type: BANNER_TYPES.NEGATIVE,
|
||||||
extraButtonText: "View Status",
|
extraButtonText: "View Status",
|
||||||
extraButtonAction: () => window.open("https://status.budibase.com/"),
|
extraButtonAction: () => window.open("https://status.budibase.com/"),
|
||||||
}
|
}
|
||||||
|
|
||||||
await show(config)
|
await queue([config])
|
||||||
|
}
|
||||||
|
|
||||||
|
const queue = async entries => {
|
||||||
|
const priority = {
|
||||||
|
[BANNER_TYPES.NEGATIVE]: 0,
|
||||||
|
[BANNER_TYPES.INFO]: 1,
|
||||||
|
}
|
||||||
|
banner.update(store => {
|
||||||
|
const sorted = [...store.messages, ...entries].sort((a, b) => {
|
||||||
|
if (priority[a.type] == priority[b.type]) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return priority[a.type] < priority[b.type] ? -1 : 1
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
...store,
|
||||||
|
messages: sorted,
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe: banner.subscribe,
|
subscribe: banner.subscribe,
|
||||||
showStatus,
|
showStatus,
|
||||||
|
show,
|
||||||
|
queue,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
export let tooltip = ""
|
export let tooltip = ""
|
||||||
export let size = "M"
|
export let size = "M"
|
||||||
|
export let disabled = true
|
||||||
|
|
||||||
let showTooltip = false
|
let showTooltip = false
|
||||||
</script>
|
</script>
|
||||||
|
@ -19,7 +20,7 @@
|
||||||
on:mouseleave={() => (showTooltip = false)}
|
on:mouseleave={() => (showTooltip = false)}
|
||||||
on:focus
|
on:focus
|
||||||
>
|
>
|
||||||
<Icon name="InfoOutline" size="S" disabled={true} />
|
<Icon name="InfoOutline" size="S" {disabled} />
|
||||||
</div>
|
</div>
|
||||||
{#if showTooltip}
|
{#if showTooltip}
|
||||||
<div class="tooltip">
|
<div class="tooltip">
|
||||||
|
@ -54,7 +55,6 @@
|
||||||
transform: scale(0.75);
|
transform: scale(0.75);
|
||||||
}
|
}
|
||||||
.icon-small {
|
.icon-small {
|
||||||
margin-top: -2px;
|
margin-bottom: -2px;
|
||||||
margin-bottom: -5px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -34,6 +34,7 @@ export { default as Layout } from "./Layout/Layout.svelte"
|
||||||
export { default as Page } from "./Layout/Page.svelte"
|
export { default as Page } from "./Layout/Page.svelte"
|
||||||
export { default as Link } from "./Link/Link.svelte"
|
export { default as Link } from "./Link/Link.svelte"
|
||||||
export { default as Tooltip } from "./Tooltip/Tooltip.svelte"
|
export { default as Tooltip } from "./Tooltip/Tooltip.svelte"
|
||||||
|
export { default as TooltipWrapper } from "./Tooltip/TooltipWrapper.svelte"
|
||||||
export { default as Menu } from "./Menu/Menu.svelte"
|
export { default as Menu } from "./Menu/Menu.svelte"
|
||||||
export { default as MenuSection } from "./Menu/Section.svelte"
|
export { default as MenuSection } from "./Menu/Section.svelte"
|
||||||
export { default as MenuSeparator } from "./Menu/Separator.svelte"
|
export { default as MenuSeparator } from "./Menu/Separator.svelte"
|
||||||
|
@ -94,7 +95,7 @@ export { default as clickOutside } from "./Actions/click_outside"
|
||||||
|
|
||||||
// Stores
|
// Stores
|
||||||
export { notifications, createNotificationStore } from "./Stores/notifications"
|
export { notifications, createNotificationStore } from "./Stores/notifications"
|
||||||
export { banner } from "./Stores/banner"
|
export { banner, BANNER_TYPES } from "./Stores/banner"
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
export * as Helpers from "./helpers"
|
export * as Helpers from "./helpers"
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import { NotificationDisplay, BannerDisplay } from "@budibase/bbui"
|
import { NotificationDisplay, BannerDisplay } from "@budibase/bbui"
|
||||||
import { parse, stringify } from "qs"
|
import { parse, stringify } from "qs"
|
||||||
import HelpIcon from "components/common/HelpIcon.svelte"
|
import HelpIcon from "components/common/HelpIcon.svelte"
|
||||||
|
import LicensingOverlays from "components/portal/licensing/LicensingOverlays.svelte"
|
||||||
|
|
||||||
const queryHandler = { parse, stringify }
|
const queryHandler = { parse, stringify }
|
||||||
</script>
|
</script>
|
||||||
|
@ -12,6 +13,9 @@
|
||||||
<BannerDisplay />
|
<BannerDisplay />
|
||||||
|
|
||||||
<NotificationDisplay />
|
<NotificationDisplay />
|
||||||
|
|
||||||
|
<LicensingOverlays />
|
||||||
|
|
||||||
<Router {routes} config={{ queryHandler }} />
|
<Router {routes} config={{ queryHandler }} />
|
||||||
<div class="modal-container" />
|
<div class="modal-container" />
|
||||||
<HelpIcon />
|
<HelpIcon />
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { getFrontendStore } from "./store/frontend"
|
import { getFrontendStore } from "./store/frontend"
|
||||||
import { getAutomationStore } from "./store/automation"
|
import { getAutomationStore } from "./store/automation"
|
||||||
|
import { getTemporalStore } from "./store/temporal"
|
||||||
import { getThemeStore } from "./store/theme"
|
import { getThemeStore } from "./store/theme"
|
||||||
import { derived } from "svelte/store"
|
import { derived } from "svelte/store"
|
||||||
import { findComponent, findComponentPath } from "./componentUtils"
|
import { findComponent, findComponentPath } from "./componentUtils"
|
||||||
|
@ -8,6 +9,7 @@ import { RoleUtils } from "@budibase/frontend-core"
|
||||||
export const store = getFrontendStore()
|
export const store = getFrontendStore()
|
||||||
export const automationStore = getAutomationStore()
|
export const automationStore = getAutomationStore()
|
||||||
export const themeStore = getThemeStore()
|
export const themeStore = getThemeStore()
|
||||||
|
export const temporalStore = getTemporalStore()
|
||||||
|
|
||||||
export const selectedScreen = derived(store, $store => {
|
export const selectedScreen = derived(store, $store => {
|
||||||
return $store.screens.find(screen => screen._id === $store.selectedScreenId)
|
return $store.screens.find(screen => screen._id === $store.selectedScreenId)
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { createLocalStorageStore } from "@budibase/frontend-core"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
|
||||||
|
export const getTemporalStore = () => {
|
||||||
|
const initialValue = {}
|
||||||
|
|
||||||
|
const localStorageKey = `bb-temporal`
|
||||||
|
const store = createLocalStorageStore(localStorageKey, initialValue)
|
||||||
|
|
||||||
|
const setExpiring = (key, data, duration) => {
|
||||||
|
const updated = {
|
||||||
|
...data,
|
||||||
|
expiry: Date.now() + duration * 1000,
|
||||||
|
}
|
||||||
|
|
||||||
|
store.update(state => ({
|
||||||
|
...state,
|
||||||
|
[key]: updated,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const getExpiring = key => {
|
||||||
|
const entry = get(store)[key]
|
||||||
|
if (!entry) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const currentExpiry = entry.expiry
|
||||||
|
if (currentExpiry < Date.now()) {
|
||||||
|
store.update(state => {
|
||||||
|
delete state[key]
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: store.subscribe,
|
||||||
|
actions: { setExpiring, getExpiring },
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,7 @@
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import TemplateCard from "components/common/TemplateCard.svelte"
|
import TemplateCard from "components/common/TemplateCard.svelte"
|
||||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||||
|
import { licensing } from "stores/portal"
|
||||||
|
|
||||||
export let templates
|
export let templates
|
||||||
|
|
||||||
|
@ -96,15 +97,17 @@
|
||||||
backgroundColour={templateEntry.background}
|
backgroundColour={templateEntry.background}
|
||||||
icon={templateEntry.icon}
|
icon={templateEntry.icon}
|
||||||
>
|
>
|
||||||
<Button
|
{#if $licensing?.usageMetrics?.apps < 100}
|
||||||
cta
|
<Button
|
||||||
on:click={() => {
|
cta
|
||||||
template = templateEntry
|
on:click={() => {
|
||||||
creationModal.show()
|
template = templateEntry
|
||||||
}}
|
creationModal.show()
|
||||||
>
|
}}
|
||||||
Use template
|
>
|
||||||
</Button>
|
Use template
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
<a
|
<a
|
||||||
href={templateEntry.url}
|
href={templateEntry.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
<script>
|
||||||
|
import { Modal, ModalContent, Body } from "@budibase/bbui"
|
||||||
|
import { auth, admin } from "stores/portal"
|
||||||
|
|
||||||
|
export let onDismiss = () => {}
|
||||||
|
export let onShow = () => {}
|
||||||
|
|
||||||
|
let accountDowngradeModal
|
||||||
|
|
||||||
|
$: accountUrl = $admin.accountPortalUrl
|
||||||
|
$: upgradeUrl = `${accountUrl}/portal/upgrade`
|
||||||
|
|
||||||
|
export function show() {
|
||||||
|
accountDowngradeModal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hide() {
|
||||||
|
accountDowngradeModal.hide()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:this={accountDowngradeModal} on:show={onShow} on:hide={onDismiss}>
|
||||||
|
<ModalContent
|
||||||
|
title="Your account is now on the Free plan"
|
||||||
|
size="M"
|
||||||
|
showCancelButton={$auth.user.accountPortalAccess}
|
||||||
|
confirmText={$auth.user.accountPortalAccess ? "Upgrade" : "Confirm"}
|
||||||
|
onConfirm={$auth.user.accountPortalAccess
|
||||||
|
? () => {
|
||||||
|
window.location.href = upgradeUrl
|
||||||
|
}
|
||||||
|
: null}
|
||||||
|
>
|
||||||
|
<Body>
|
||||||
|
The payment for your subscription has failed and we have downgraded your
|
||||||
|
account to the <span class="free-plan">Free plan</span>.
|
||||||
|
</Body>
|
||||||
|
<Body>Upgrade to restore full functionality.</Body>
|
||||||
|
{#if !$auth.user.accountPortalAccess}
|
||||||
|
<Body>Please contact the account holder to upgrade.</Body>
|
||||||
|
{/if}
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.free-plan {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,47 @@
|
||||||
|
<script>
|
||||||
|
import { Modal, ModalContent, Body } from "@budibase/bbui"
|
||||||
|
import { auth, admin } from "stores/portal"
|
||||||
|
|
||||||
|
export let onDismiss = () => {}
|
||||||
|
|
||||||
|
let appLimitModal
|
||||||
|
|
||||||
|
$: accountUrl = $admin.accountPortalUrl
|
||||||
|
$: upgradeUrl = `${accountUrl}/portal/upgrade`
|
||||||
|
|
||||||
|
export function show() {
|
||||||
|
appLimitModal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hide() {
|
||||||
|
appLimitModal.hide()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:this={appLimitModal} on:hide={onDismiss}>
|
||||||
|
<ModalContent
|
||||||
|
title="Upgrade to get more apps "
|
||||||
|
size="M"
|
||||||
|
showCancelButton={false}
|
||||||
|
confirmText={$auth.user.accountPortalAccess ? "Upgrade" : "Confirm"}
|
||||||
|
onConfirm={$auth.user.accountPortalAccess
|
||||||
|
? () => {
|
||||||
|
window.location.href = upgradeUrl
|
||||||
|
}
|
||||||
|
: null}
|
||||||
|
>
|
||||||
|
<Body>
|
||||||
|
You are currently on our <span class="free-plan">Free plan</span>. Upgrade
|
||||||
|
to our Pro plan to get unlimited apps and additional features.
|
||||||
|
</Body>
|
||||||
|
{#if !$auth.user.accountPortalAccess}
|
||||||
|
<Body>Please contact the account holder to upgrade.</Body>
|
||||||
|
{/if}
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.free-plan {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,78 @@
|
||||||
|
<script>
|
||||||
|
import { Modal, ModalContent, Body, TooltipWrapper } from "@budibase/bbui"
|
||||||
|
import { licensing, auth, admin } from "stores/portal"
|
||||||
|
|
||||||
|
export let onDismiss = () => {}
|
||||||
|
export let onShow = () => {}
|
||||||
|
|
||||||
|
let dayPassModal
|
||||||
|
|
||||||
|
$: accountUrl = $admin.accountPortalUrl
|
||||||
|
$: upgradeUrl = `${accountUrl}/portal/upgrade`
|
||||||
|
|
||||||
|
$: daysRemaining = $licensing.quotaResetDaysRemaining
|
||||||
|
$: quotaResetDate = $licensing.quotaResetDate
|
||||||
|
$: dayPassesUsed = $licensing.usageMetrics?.dayPasses
|
||||||
|
$: dayPassesTitle =
|
||||||
|
dayPassesUsed >= 100
|
||||||
|
? "You have run out of Day Passes"
|
||||||
|
: "You are almost out of Day Passes"
|
||||||
|
$: dayPassesBody =
|
||||||
|
dayPassesUsed >= 100
|
||||||
|
? "Upgrade your account to bring your apps back online."
|
||||||
|
: "Upgrade your account to prevent your apps from going offline."
|
||||||
|
|
||||||
|
export function show() {
|
||||||
|
dayPassModal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hide() {
|
||||||
|
dayPassModal.hide()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:this={dayPassModal} on:show={onShow} on:hide={onDismiss}>
|
||||||
|
{#if $auth.user.accountPortalAccess}
|
||||||
|
<ModalContent
|
||||||
|
title={dayPassesTitle}
|
||||||
|
size="M"
|
||||||
|
confirmText="Upgrade"
|
||||||
|
onConfirm={() => {
|
||||||
|
window.location.href = upgradeUrl
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Body>
|
||||||
|
You have used <span class="daypass_percent">{dayPassesUsed}%</span> of
|
||||||
|
your plans Day Passes with {daysRemaining} day{daysRemaining == 1
|
||||||
|
? ""
|
||||||
|
: "s"} remaining.
|
||||||
|
<span class="tooltip">
|
||||||
|
<TooltipWrapper tooltip={quotaResetDate} size="S" />
|
||||||
|
</span>
|
||||||
|
</Body>
|
||||||
|
<Body>{dayPassesBody}</Body>
|
||||||
|
</ModalContent>
|
||||||
|
{:else}
|
||||||
|
<ModalContent title={dayPassesTitle} size="M" showCancelButton={false}>
|
||||||
|
<Body>
|
||||||
|
You have used <span class="daypass_percent">{dayPassesUsed}%</span> of
|
||||||
|
your plans Day Passes with {daysRemaining} day{daysRemaining == 1
|
||||||
|
? ""
|
||||||
|
: "s"} remaining.
|
||||||
|
<span class="tooltip">
|
||||||
|
<TooltipWrapper tooltip={quotaResetDate} size="S" />
|
||||||
|
</span>
|
||||||
|
</Body>
|
||||||
|
<Body>Please contact your account holder to upgrade.</Body>
|
||||||
|
</ModalContent>
|
||||||
|
{/if}
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.tooltip {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.tooltip :global(.icon-container) {
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,112 @@
|
||||||
|
<script>
|
||||||
|
import { licensing, auth } from "stores/portal"
|
||||||
|
import { temporalStore } from "builderStore"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import DayPassWarningModal from "./DayPassWarningModal.svelte"
|
||||||
|
import PaymentFailedModal from "./PaymentFailedModal.svelte"
|
||||||
|
import AccountDowngradedModal from "./AccountDowngradedModal.svelte"
|
||||||
|
import { ExpiringKeys } from "./constants"
|
||||||
|
import { getBanners } from "./licensingBanners"
|
||||||
|
import { banner } from "@budibase/bbui"
|
||||||
|
|
||||||
|
const oneDayInSeconds = 86400
|
||||||
|
|
||||||
|
let queuedBanners = []
|
||||||
|
let queuedModals = []
|
||||||
|
let dayPassModal
|
||||||
|
let paymentFailedModal
|
||||||
|
let accountDowngradeModal
|
||||||
|
let userLoaded = false
|
||||||
|
let loaded = false
|
||||||
|
let licensingLoaded = false
|
||||||
|
let currentModalCfg = null
|
||||||
|
|
||||||
|
const processModals = () => {
|
||||||
|
const defaultCacheFn = key => {
|
||||||
|
temporalStore.actions.setExpiring(key, {}, oneDayInSeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
const dismissableModals = [
|
||||||
|
{
|
||||||
|
key: ExpiringKeys.LICENSING_DAYPASS_WARNING_MODAL,
|
||||||
|
criteria: () => {
|
||||||
|
return $licensing?.usageMetrics?.dayPasses >= 90
|
||||||
|
},
|
||||||
|
action: () => {
|
||||||
|
dayPassModal.show()
|
||||||
|
},
|
||||||
|
cache: () => {
|
||||||
|
defaultCacheFn(ExpiringKeys.LICENSING_DAYPASS_WARNING_MODAL)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: ExpiringKeys.LICENSING_PAYMENT_FAILED,
|
||||||
|
criteria: () => {
|
||||||
|
return $licensing.accountPastDue && !$licensing.isFreePlan()
|
||||||
|
},
|
||||||
|
action: () => {
|
||||||
|
paymentFailedModal.show()
|
||||||
|
},
|
||||||
|
cache: () => {
|
||||||
|
defaultCacheFn(ExpiringKeys.LICENSING_PAYMENT_FAILED)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: ExpiringKeys.LICENSING_ACCOUNT_DOWNGRADED_MODAL,
|
||||||
|
criteria: () => {
|
||||||
|
return $licensing?.accountDowngraded
|
||||||
|
},
|
||||||
|
action: () => {
|
||||||
|
accountDowngradeModal.show()
|
||||||
|
},
|
||||||
|
cache: () => {
|
||||||
|
defaultCacheFn(ExpiringKeys.LICENSING_ACCOUNT_DOWNGRADED_MODAL)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
return dismissableModals.filter(modal => {
|
||||||
|
return !temporalStore.actions.getExpiring(modal.key) && modal.criteria()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const showNextModal = () => {
|
||||||
|
if (currentModalCfg) {
|
||||||
|
currentModalCfg.cache()
|
||||||
|
}
|
||||||
|
if (queuedModals.length) {
|
||||||
|
currentModalCfg = queuedModals.shift()
|
||||||
|
currentModalCfg.action()
|
||||||
|
} else {
|
||||||
|
currentModalCfg = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (userLoaded && licensingLoaded && loaded) {
|
||||||
|
queuedModals = processModals()
|
||||||
|
queuedBanners = getBanners()
|
||||||
|
showNextModal()
|
||||||
|
banner.queue(queuedBanners)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
auth.subscribe(state => {
|
||||||
|
if (state.user && !userLoaded) {
|
||||||
|
userLoaded = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
licensing.subscribe(state => {
|
||||||
|
if (state.usageMetrics && !licensingLoaded) {
|
||||||
|
licensingLoaded = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
loaded = true
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DayPassWarningModal bind:this={dayPassModal} onDismiss={showNextModal} />
|
||||||
|
<PaymentFailedModal bind:this={paymentFailedModal} onDismiss={showNextModal} />
|
||||||
|
<AccountDowngradedModal
|
||||||
|
bind:this={accountDowngradeModal}
|
||||||
|
onDismiss={showNextModal}
|
||||||
|
/>
|
|
@ -0,0 +1,84 @@
|
||||||
|
<script>
|
||||||
|
import { Modal, ModalContent, Body, TooltipWrapper } from "@budibase/bbui"
|
||||||
|
import { auth, admin, licensing } from "stores/portal"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
|
export let onDismiss = () => {}
|
||||||
|
export let onShow = () => {}
|
||||||
|
|
||||||
|
let paymentFailedModal
|
||||||
|
let pastDueEndDate
|
||||||
|
|
||||||
|
const paymentFailedTitle = "Payment failed"
|
||||||
|
$: accountUrl = $admin.accountPortalUrl
|
||||||
|
$: upgradeUrl = `${accountUrl}/portal/upgrade`
|
||||||
|
|
||||||
|
export function show() {
|
||||||
|
paymentFailedModal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hide() {
|
||||||
|
paymentFailedModal.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
licensing.subscribe(state => {
|
||||||
|
pastDueEndDate = state.pastDueEndDate
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:this={paymentFailedModal} on:show={onShow} on:hide={onDismiss}>
|
||||||
|
{#if $auth.user.accountPortalAccess}
|
||||||
|
<ModalContent
|
||||||
|
title={paymentFailedTitle}
|
||||||
|
size="M"
|
||||||
|
confirmText="Upgrade"
|
||||||
|
onConfirm={() => {
|
||||||
|
window.location.href = upgradeUrl
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Body>The payment for your subscription has failed</Body>
|
||||||
|
<Body>
|
||||||
|
Please upgrade your billing details before your account gets downgraded
|
||||||
|
to the free plan
|
||||||
|
</Body>
|
||||||
|
<Body weight={800}>
|
||||||
|
<div class="tooltip-root">
|
||||||
|
{`${$licensing.pastDueDaysRemaining} day${
|
||||||
|
$licensing.pastDueDaysRemaining == 1 ? "" : "s"
|
||||||
|
} remaining`}
|
||||||
|
<span class="tooltip">
|
||||||
|
<TooltipWrapper tooltip={pastDueEndDate} size="S" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Body>
|
||||||
|
</ModalContent>
|
||||||
|
{:else}
|
||||||
|
<ModalContent title={paymentFailedTitle} size="M" showCancelButton={false}>
|
||||||
|
<Body>The payment for your subscription has failed</Body>
|
||||||
|
<Body>
|
||||||
|
Please upgrade your billing details before your account gets downgraded
|
||||||
|
to the free plan
|
||||||
|
</Body>
|
||||||
|
<Body>Please contact your account holder.</Body>
|
||||||
|
<Body weight={800}>
|
||||||
|
<div class="tooltip-root">
|
||||||
|
{`${$licensing.pastDueDaysRemaining} day${
|
||||||
|
$licensing.pastDueDaysRemaining == 1 ? "" : "s"
|
||||||
|
} remaining`}
|
||||||
|
<span class="tooltip">
|
||||||
|
<TooltipWrapper tooltip={pastDueEndDate} size="S" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Body>
|
||||||
|
</ModalContent>
|
||||||
|
{/if}
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.tooltip-root {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,15 @@
|
||||||
|
export const ExpiringKeys = {
|
||||||
|
LICENSING_DAYPASS_WARNING_MODAL: "licensing_daypass_warning_90_modal",
|
||||||
|
LICENSING_DAYPASS_WARNING_BANNER: "licensing_daypass_warning_90_banner",
|
||||||
|
LICENSING_PAYMENT_FAILED: "licensing_payment_failed",
|
||||||
|
LICENSING_ACCOUNT_DOWNGRADED_MODAL: "licensing_account_downgraded_modal",
|
||||||
|
LICENSING_APP_LIMIT_MODAL: "licensing_app_limit_modal",
|
||||||
|
LICENSING_ROWS_WARNING_BANNER: "licensing_rows_warning_banner",
|
||||||
|
LICENSING_AUTOMATIONS_WARNING_BANNER: "licensing_automations_warning_banner",
|
||||||
|
LICENSING_QUERIES_WARNING_BANNER: "licensing_queries_warning_banner",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StripeStatus = {
|
||||||
|
PAST_DUE: "past_due",
|
||||||
|
ACTIVE: "active",
|
||||||
|
}
|
|
@ -0,0 +1,156 @@
|
||||||
|
import { ExpiringKeys } from "./constants"
|
||||||
|
import { temporalStore } from "builderStore"
|
||||||
|
import { admin, auth, licensing } from "stores/portal"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import { BANNER_TYPES } from "@budibase/bbui"
|
||||||
|
|
||||||
|
const oneDayInSeconds = 86400
|
||||||
|
|
||||||
|
const defaultCacheFn = key => {
|
||||||
|
temporalStore.actions.setExpiring(key, {}, oneDayInSeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultAction = key => {
|
||||||
|
if (!get(auth).user.accountPortalAccess) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
extraButtonText: "Upgrade Plan",
|
||||||
|
extraButtonAction: () => {
|
||||||
|
defaultCacheFn(key)
|
||||||
|
window.location.href = `${get(admin).accountPortalUrl}/portal/upgrade`
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildUsageInfoBanner = (
|
||||||
|
metricKey,
|
||||||
|
metricLabel,
|
||||||
|
cacheKey,
|
||||||
|
percentageThreshold,
|
||||||
|
customMessage
|
||||||
|
) => {
|
||||||
|
const appAuth = get(auth)
|
||||||
|
const appLicensing = get(licensing)
|
||||||
|
|
||||||
|
const displayPercent =
|
||||||
|
appLicensing?.usageMetrics[metricKey] > 100
|
||||||
|
? 100
|
||||||
|
: appLicensing?.usageMetrics[metricKey]
|
||||||
|
|
||||||
|
let bannerConfig = {
|
||||||
|
key: cacheKey,
|
||||||
|
type: BANNER_TYPES.INFO,
|
||||||
|
onChange: () => {
|
||||||
|
defaultCacheFn(cacheKey)
|
||||||
|
},
|
||||||
|
message: customMessage
|
||||||
|
? customMessage
|
||||||
|
: `You have used ${displayPercent}% of your monthly usage of ${metricLabel} with ${
|
||||||
|
appLicensing.quotaResetDaysRemaining
|
||||||
|
} day${
|
||||||
|
appLicensing.quotaResetDaysRemaining == 1 ? "" : "s"
|
||||||
|
} remaining. ${
|
||||||
|
appAuth.user.accountPortalAccess
|
||||||
|
? ""
|
||||||
|
: "Please contact your account holder to upgrade"
|
||||||
|
}`,
|
||||||
|
criteria: () => {
|
||||||
|
return appLicensing?.usageMetrics[metricKey] >= percentageThreshold
|
||||||
|
},
|
||||||
|
tooltip: appLicensing?.quotaResetDate,
|
||||||
|
}
|
||||||
|
|
||||||
|
return !get(auth).user.accountPortalAccess
|
||||||
|
? bannerConfig
|
||||||
|
: {
|
||||||
|
...bannerConfig,
|
||||||
|
...defaultAction(cacheKey),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildDayPassBanner = () => {
|
||||||
|
const appAuth = get(auth)
|
||||||
|
const appLicensing = get(licensing)
|
||||||
|
if (get(licensing)?.usageMetrics["dayPasses"] >= 100) {
|
||||||
|
return {
|
||||||
|
key: "max_dayPasses",
|
||||||
|
type: BANNER_TYPES.NEGATIVE,
|
||||||
|
criteria: () => {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
message: `Your apps are currently offline. You have exceeded your plans limit for Day Passes. ${
|
||||||
|
appAuth.user.accountPortalAccess
|
||||||
|
? ""
|
||||||
|
: "Please contact your account holder to upgrade."
|
||||||
|
}`,
|
||||||
|
...defaultAction(),
|
||||||
|
showCloseButton: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildUsageInfoBanner(
|
||||||
|
"dayPasses",
|
||||||
|
"Day Passes",
|
||||||
|
ExpiringKeys.LICENSING_DAYPASS_WARNING_BANNER,
|
||||||
|
90,
|
||||||
|
`You have used ${
|
||||||
|
appLicensing?.usageMetrics["dayPasses"]
|
||||||
|
}% of your monthly usage of Day Passes with ${
|
||||||
|
appLicensing?.quotaResetDaysRemaining
|
||||||
|
} day${
|
||||||
|
get(licensing).quotaResetDaysRemaining == 1 ? "" : "s"
|
||||||
|
} remaining. All apps will be taken offline if this limit is reached. ${
|
||||||
|
appAuth.user.accountPortalAccess
|
||||||
|
? ""
|
||||||
|
: "Please contact your account holder to upgrade."
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildPaymentFailedBanner = () => {
|
||||||
|
return {
|
||||||
|
key: "payment_Failed",
|
||||||
|
type: BANNER_TYPES.NEGATIVE,
|
||||||
|
criteria: () => {
|
||||||
|
return get(licensing)?.accountPastDue && !get(licensing).isFreePlan()
|
||||||
|
},
|
||||||
|
message: `Payment Failed - Please update your billing details or your account will be downgrades in
|
||||||
|
${get(licensing)?.pastDueDaysRemaining} day${
|
||||||
|
get(licensing)?.pastDueDaysRemaining == 1 ? "" : "s"
|
||||||
|
}`,
|
||||||
|
...defaultAction(),
|
||||||
|
showCloseButton: false,
|
||||||
|
tooltip: get(licensing).pastDueEndDate,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getBanners = () => {
|
||||||
|
return [
|
||||||
|
buildPaymentFailedBanner(),
|
||||||
|
buildDayPassBanner(ExpiringKeys.LICENSING_DAYPASS_WARNING_BANNER),
|
||||||
|
buildUsageInfoBanner(
|
||||||
|
"rows",
|
||||||
|
"Rows",
|
||||||
|
ExpiringKeys.LICENSING_ROWS_WARNING_BANNER,
|
||||||
|
90
|
||||||
|
),
|
||||||
|
buildUsageInfoBanner(
|
||||||
|
"automations",
|
||||||
|
"Automations",
|
||||||
|
ExpiringKeys.LICENSING_AUTOMATIONS_WARNING_BANNER,
|
||||||
|
90
|
||||||
|
),
|
||||||
|
buildUsageInfoBanner(
|
||||||
|
"queries",
|
||||||
|
"Queries",
|
||||||
|
ExpiringKeys.LICENSING_QUERIES_WARNING_BANNER,
|
||||||
|
90
|
||||||
|
),
|
||||||
|
].filter(licensingBanner => {
|
||||||
|
return (
|
||||||
|
!temporalStore.actions.getExpiring(licensingBanner.key) &&
|
||||||
|
licensingBanner.criteria()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,10 +1,15 @@
|
||||||
<script>
|
<script>
|
||||||
import { Body, ProgressBar, Label } from "@budibase/bbui"
|
import { Body, ProgressBar, Heading, Icon, Link } from "@budibase/bbui"
|
||||||
|
import { admin, auth } from "../../stores/portal"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
export let usage
|
export let usage
|
||||||
|
export let warnWhenFull = false
|
||||||
|
|
||||||
let percentage
|
let percentage
|
||||||
let unlimited = false
|
let unlimited = false
|
||||||
|
let showWarning = false
|
||||||
|
|
||||||
|
$: accountPortalAccess = $auth?.user?.accountPortalAccess
|
||||||
|
|
||||||
const isUnlimited = () => {
|
const isUnlimited = () => {
|
||||||
if (usage.total === -1) {
|
if (usage.total === -1) {
|
||||||
|
@ -14,29 +19,62 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPercentage = () => {
|
const getPercentage = () => {
|
||||||
return Math.min(Math.ceil((usage.used / usage.total) * 100), 100)
|
return (usage.used / usage.total) * 100
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade`
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
unlimited = isUnlimited()
|
unlimited = isUnlimited()
|
||||||
percentage = getPercentage()
|
percentage = getPercentage()
|
||||||
|
if (warnWhenFull && percentage === 100) {
|
||||||
|
showWarning = true
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="usage">
|
<div class="usage">
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<Label size="XL">{usage.name}</Label>
|
<div class="header-container">
|
||||||
|
{#if showWarning}
|
||||||
|
<Icon name="Alert" />
|
||||||
|
{/if}
|
||||||
|
<div class="heading header-item">
|
||||||
|
<Heading size="XS" weight="light">{usage.name}</Heading>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{#if unlimited}
|
{#if unlimited}
|
||||||
<Body size="S">{usage.used}</Body>
|
<Body size="S">{usage.used} / Unlimited</Body>
|
||||||
{:else}
|
{:else}
|
||||||
<Body size="S">{usage.used} / {usage.total}</Body>
|
<Body size="S">{usage.used} / {usage.total}</Body>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{#if unlimited}
|
{#if unlimited}
|
||||||
<Body size="S">Unlimited</Body>
|
<ProgressBar
|
||||||
|
showPercentage={false}
|
||||||
|
width={"100%"}
|
||||||
|
duration={1}
|
||||||
|
value={100}
|
||||||
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<ProgressBar width={"100%"} duration={1} value={percentage} />
|
<ProgressBar
|
||||||
|
color={showWarning ? "red" : "green"}
|
||||||
|
showPercentage={false}
|
||||||
|
width={"100%"}
|
||||||
|
duration={1}
|
||||||
|
value={percentage}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{#if showWarning}
|
||||||
|
<Body size="S">
|
||||||
|
To get more {usage.name.toLowerCase()}
|
||||||
|
{#if accountPortalAccess}
|
||||||
|
<Link href={upgradeUrl}>upgrade your plan</Link>
|
||||||
|
{:else}
|
||||||
|
contact your account holder
|
||||||
|
{/if}
|
||||||
|
</Body>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -51,6 +89,13 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: var(--spacing-m);
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.header-container {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.heading {
|
||||||
|
margin-top: 3px;
|
||||||
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Detail,
|
||||||
|
Button,
|
||||||
|
Heading,
|
||||||
|
Layout,
|
||||||
|
Body,
|
||||||
|
TooltipWrapper,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let description = ""
|
||||||
|
export let title = ""
|
||||||
|
export let primaryAction
|
||||||
|
export let secondaryAction
|
||||||
|
export let primaryActionText
|
||||||
|
export let secondaryActionText
|
||||||
|
export let primaryCta = true
|
||||||
|
export let textRows = []
|
||||||
|
|
||||||
|
$: primaryDefined = primaryAction && primaryActionText
|
||||||
|
$: secondaryDefined = secondaryAction && secondaryActionText
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="dash-card">
|
||||||
|
<div class="dash-card-header">
|
||||||
|
<div class="header-info">
|
||||||
|
<Layout gap="XS">
|
||||||
|
<div class="dash-card-title">
|
||||||
|
<Detail size="M">{description}</Detail>
|
||||||
|
</div>
|
||||||
|
<Heading size="M">{title}</Heading>
|
||||||
|
{#if textRows.length}
|
||||||
|
<div class="text-rows">
|
||||||
|
{#each textRows as row}
|
||||||
|
{#if row.tooltip}
|
||||||
|
<TooltipWrapper tooltip={row.tooltip}>
|
||||||
|
<Body>{row.message}</Body>
|
||||||
|
</TooltipWrapper>
|
||||||
|
{:else}
|
||||||
|
<Body>{row.message}</Body>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
{#if secondaryDefined}
|
||||||
|
<div>
|
||||||
|
<Button newStyles secondary on:click={secondaryAction}
|
||||||
|
>{secondaryActionText}</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if primaryDefined}
|
||||||
|
<div class="primary-button">
|
||||||
|
<Button cta={primaryCta} on:click={primaryAction}
|
||||||
|
>{primaryActionText}</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dash-card-body">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.dash-card {
|
||||||
|
background: var(--spectrum-alias-background-color-primary);
|
||||||
|
border-radius: var(--border-radius-s);
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 150px;
|
||||||
|
}
|
||||||
|
.dash-card-header {
|
||||||
|
padding: 15px 25px 20px;
|
||||||
|
border-bottom: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.dash-card-body {
|
||||||
|
padding: 25px 30px;
|
||||||
|
}
|
||||||
|
.dash-card-title :global(.spectrum-Detail) {
|
||||||
|
color: var(
|
||||||
|
--spectrum-sidenav-heading-text-color,
|
||||||
|
var(--spectrum-global-color-gray-700)
|
||||||
|
);
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.header-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.header-actions {
|
||||||
|
flex: 1;
|
||||||
|
margin-top: 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.header-actions :global(:first-child) {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
.text-rows {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 900px) {
|
||||||
|
.dash-card-header {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
justify-content: flex-start "bul";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as Usage } from "./Usage.svelte"
|
||||||
|
export { default as DashCard } from "./UsageDashCard.svelte"
|
|
@ -58,6 +58,13 @@ export const DefaultAppTheme = {
|
||||||
navTextColor: "var(--spectrum-global-color-gray-800)",
|
navTextColor: "var(--spectrum-global-color-gray-800)",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const PlanType = {
|
||||||
|
FREE: "free",
|
||||||
|
PRO: "pro",
|
||||||
|
BUSINESS: "business",
|
||||||
|
ENTERPRISE: "enterprise",
|
||||||
|
}
|
||||||
|
|
||||||
export const PluginSource = {
|
export const PluginSource = {
|
||||||
URL: "URL",
|
URL: "URL",
|
||||||
NPM: "NPM",
|
NPM: "NPM",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { isActive, redirect, params } from "@roxi/routify"
|
import { isActive, redirect, params } from "@roxi/routify"
|
||||||
import { admin, auth } from "stores/portal"
|
import { admin, auth, licensing } from "stores/portal"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { CookieUtils, Constants } from "@budibase/frontend-core"
|
import { CookieUtils, Constants } from "@budibase/frontend-core"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
@ -63,6 +63,10 @@
|
||||||
await auth.getSelf()
|
await auth.getSelf()
|
||||||
await admin.init()
|
await admin.init()
|
||||||
|
|
||||||
|
if ($auth.user) {
|
||||||
|
await licensing.init()
|
||||||
|
}
|
||||||
|
|
||||||
// Set init info if present
|
// Set init info if present
|
||||||
if ($params["?template"]) {
|
if ($params["?template"]) {
|
||||||
await auth.setInitInfo({ init_template: $params["?template"] })
|
await auth.setInitInfo({ init_template: $params["?template"] })
|
||||||
|
|
|
@ -13,13 +13,14 @@
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { apps, organisation, auth, groups } from "stores/portal"
|
import { apps, organisation, auth, groups, licensing } from "stores/portal"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { AppStatus } from "constants"
|
import { AppStatus } from "constants"
|
||||||
import { gradient } from "actions"
|
import { gradient } from "actions"
|
||||||
import UpdateUserInfoModal from "components/settings/UpdateUserInfoModal.svelte"
|
import UpdateUserInfoModal from "components/settings/UpdateUserInfoModal.svelte"
|
||||||
import ChangePasswordModal from "components/settings/ChangePasswordModal.svelte"
|
import ChangePasswordModal from "components/settings/ChangePasswordModal.svelte"
|
||||||
import { processStringSync } from "@budibase/string-templates"
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
|
import Spaceman from "assets/bb-space-man.svg"
|
||||||
import Logo from "assets/bb-emblem.svg"
|
import Logo from "assets/bb-emblem.svg"
|
||||||
|
|
||||||
let loaded = false
|
let loaded = false
|
||||||
|
@ -91,7 +92,7 @@
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<Layout noPadding>
|
<Layout noPadding>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
<img class="logo" alt="logo" src={$organisation.logoUrl || Logo} />
|
||||||
<ActionMenu align="right" dataCy="user-menu">
|
<ActionMenu align="right" dataCy="user-menu">
|
||||||
<div slot="control" class="avatar">
|
<div slot="control" class="avatar">
|
||||||
<Avatar
|
<Avatar
|
||||||
|
@ -131,7 +132,17 @@
|
||||||
</Body>
|
</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
<Divider />
|
<Divider />
|
||||||
{#if userApps.length}
|
{#if $licensing.usageMetrics.dayPasses >= 100}
|
||||||
|
<div>
|
||||||
|
<Layout gap="S" justifyItems="center">
|
||||||
|
<img class="spaceman" alt="spaceman" src={Spaceman} />
|
||||||
|
<Heading size="M">
|
||||||
|
{"Your apps are currently offline."}
|
||||||
|
</Heading>
|
||||||
|
Please contact the account holder to get them back online.
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
{:else if userApps.length}
|
||||||
<Heading>Apps</Heading>
|
<Heading>Apps</Heading>
|
||||||
<div class="group">
|
<div class="group">
|
||||||
<Layout gap="S" noPadding>
|
<Layout gap="S" noPadding>
|
||||||
|
@ -194,10 +205,13 @@
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
img {
|
img.logo {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
margin-bottom: -12px;
|
margin-bottom: -12px;
|
||||||
}
|
}
|
||||||
|
img.spaceman {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
.avatar {
|
.avatar {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto auto;
|
grid-template-columns: auto auto;
|
||||||
|
|
|
@ -13,8 +13,7 @@
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import ConfigChecklist from "components/common/ConfigChecklist.svelte"
|
import ConfigChecklist from "components/common/ConfigChecklist.svelte"
|
||||||
import { organisation, auth } from "stores/portal"
|
import { organisation, auth, admin as adminStore } from "stores/portal"
|
||||||
import { admin as adminStore } from "stores/portal"
|
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import UpdateUserInfoModal from "components/settings/UpdateUserInfoModal.svelte"
|
import UpdateUserInfoModal from "components/settings/UpdateUserInfoModal.svelte"
|
||||||
import ChangePasswordModal from "components/settings/ChangePasswordModal.svelte"
|
import ChangePasswordModal from "components/settings/ChangePasswordModal.svelte"
|
||||||
|
@ -37,14 +36,6 @@
|
||||||
href: "/builder/portal/apps",
|
href: "/builder/portal/apps",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
if (isEnabled(FEATURE_FLAGS.LICENSING)) {
|
|
||||||
menu = menu.concat([
|
|
||||||
{
|
|
||||||
title: "Usage",
|
|
||||||
href: "/builder/portal/settings/usage",
|
|
||||||
},
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
if (admin) {
|
if (admin) {
|
||||||
menu = menu.concat([
|
menu = menu.concat([
|
||||||
|
@ -53,9 +44,20 @@
|
||||||
href: "/builder/portal/manage/users",
|
href: "/builder/portal/manage/users",
|
||||||
heading: "Manage",
|
heading: "Manage",
|
||||||
},
|
},
|
||||||
|
isEnabled(FEATURE_FLAGS.USER_GROUPS)
|
||||||
|
? {
|
||||||
|
title: "User Groups",
|
||||||
|
href: "/builder/portal/manage/groups",
|
||||||
|
badge: "New",
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
{ title: "Auth", href: "/builder/portal/manage/auth" },
|
{ title: "Auth", href: "/builder/portal/manage/auth" },
|
||||||
{ title: "Email", href: "/builder/portal/manage/email" },
|
{ title: "Email", href: "/builder/portal/manage/email" },
|
||||||
{ title: "Plugins", href: "/builder/portal/manage/plugins" },
|
{
|
||||||
|
title: "Plugins",
|
||||||
|
href: "/builder/portal/manage/plugins",
|
||||||
|
badge: "New",
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: "Organisation",
|
title: "Organisation",
|
||||||
|
@ -68,15 +70,6 @@
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
if (isEnabled(FEATURE_FLAGS.USER_GROUPS)) {
|
|
||||||
let item = {
|
|
||||||
title: "User Groups",
|
|
||||||
href: "/builder/portal/manage/groups",
|
|
||||||
}
|
|
||||||
|
|
||||||
menu.splice(2, 0, item)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$adminStore.cloud) {
|
if (!$adminStore.cloud) {
|
||||||
menu = menu.concat([
|
menu = menu.concat([
|
||||||
{
|
{
|
||||||
|
@ -84,13 +77,6 @@
|
||||||
href: "/builder/portal/settings/update",
|
href: "/builder/portal/settings/update",
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
if (isEnabled(FEATURE_FLAGS.LICENSING)) {
|
|
||||||
menu = menu.concat({
|
|
||||||
title: "Upgrade",
|
|
||||||
href: "/builder/portal/settings/upgrade",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
menu = menu.concat([
|
menu = menu.concat([
|
||||||
|
@ -103,14 +89,62 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// add link to account portal if the user has access
|
// add link to account portal if the user has access
|
||||||
if ($auth?.user?.accountPortalAccess) {
|
let accountSectionAdded = false
|
||||||
|
|
||||||
|
// link out to account-portal if account holder in cloud or always in self-host
|
||||||
|
if ($auth?.user?.accountPortalAccess || (!$adminStore.cloud && admin)) {
|
||||||
|
accountSectionAdded = true
|
||||||
menu = menu.concat([
|
menu = menu.concat([
|
||||||
{
|
{
|
||||||
title: "Account",
|
title: "Account",
|
||||||
href: $adminStore.accountPortalUrl,
|
href: $adminStore.accountPortalUrl,
|
||||||
|
heading: "Account",
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isEnabled(FEATURE_FLAGS.LICENSING)) {
|
||||||
|
// always show usage in self-host or cloud if licensing enabled
|
||||||
|
menu = menu.concat([
|
||||||
|
{
|
||||||
|
title: "Usage",
|
||||||
|
href: "/builder/portal/settings/usage",
|
||||||
|
heading: accountSectionAdded ? "" : "Account",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
// show the relevant hosting upgrade page
|
||||||
|
if ($adminStore.cloud && $auth?.user?.accountPortalAccess) {
|
||||||
|
menu = menu.concat([
|
||||||
|
{
|
||||||
|
title: "Upgrade",
|
||||||
|
href: $adminStore.accountPortalUrl + "/portal/upgrade",
|
||||||
|
badge: "New",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
} else if (!$adminStore.cloud && admin) {
|
||||||
|
menu = menu.concat({
|
||||||
|
title: "Upgrade",
|
||||||
|
href: "/builder/portal/settings/upgrade",
|
||||||
|
badge: "New",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// show the billing page to licensed account holders in cloud
|
||||||
|
if (
|
||||||
|
$auth?.user?.accountPortalAccess &&
|
||||||
|
$auth.user.account.stripeCustomerId
|
||||||
|
) {
|
||||||
|
menu = menu.concat([
|
||||||
|
{
|
||||||
|
title: "Billing",
|
||||||
|
href: $adminStore.accountPortalUrl + "/portal/billing",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
menu = menu.filter(item => !!item)
|
||||||
return menu
|
return menu
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,11 +195,12 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
<Navigation>
|
<Navigation>
|
||||||
{#each menu as { title, href, heading }}
|
{#each menu as { title, href, heading, badge }}
|
||||||
<Item
|
<Item
|
||||||
on:click={hideMobileMenu}
|
on:click={hideMobileMenu}
|
||||||
selected={$isActive(href)}
|
selected={$isActive(href)}
|
||||||
{href}
|
{href}
|
||||||
|
{badge}
|
||||||
{heading}>{title}</Item
|
{heading}>{title}</Item
|
||||||
>
|
>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
@ -13,12 +13,14 @@
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||||
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
|
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
|
||||||
|
import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { templates } from "stores/portal"
|
import { templates, licensing } from "stores/portal"
|
||||||
|
|
||||||
let loaded = $templates?.length
|
let loaded = $templates?.length
|
||||||
let template
|
let template
|
||||||
let creationModal = false
|
let creationModal = false
|
||||||
|
let appLimitModal
|
||||||
let creatingApp = false
|
let creatingApp = false
|
||||||
|
|
||||||
const welcomeBody =
|
const welcomeBody =
|
||||||
|
@ -29,6 +31,8 @@
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
await templates.load()
|
await templates.load()
|
||||||
|
await licensing.getQuotaUsage()
|
||||||
|
await licensing.getUsageMetrics()
|
||||||
if ($templates?.length === 0) {
|
if ($templates?.length === 0) {
|
||||||
notifications.error(
|
notifications.error(
|
||||||
"There was a problem loading quick start templates."
|
"There was a problem loading quick start templates."
|
||||||
|
@ -41,9 +45,13 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
const initiateAppCreation = () => {
|
const initiateAppCreation = () => {
|
||||||
template = null
|
if ($licensing.usageMetrics.apps >= 100) {
|
||||||
creationModal.show()
|
appLimitModal.show()
|
||||||
creatingApp = true
|
} else {
|
||||||
|
template = null
|
||||||
|
creationModal.show()
|
||||||
|
creatingApp = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const stopAppCreation = () => {
|
const stopAppCreation = () => {
|
||||||
|
@ -52,9 +60,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const initiateAppImport = () => {
|
const initiateAppImport = () => {
|
||||||
template = { fromFile: true }
|
if ($licensing.usageMetrics.apps >= 100) {
|
||||||
creationModal.show()
|
appLimitModal.show()
|
||||||
creatingApp = true
|
} else {
|
||||||
|
template = { fromFile: true }
|
||||||
|
creationModal.show()
|
||||||
|
creatingApp = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -121,6 +133,7 @@
|
||||||
>
|
>
|
||||||
<CreateAppModal {template} />
|
<CreateAppModal {template} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
<AppLimitModal bind:this={appLimitModal} />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.title .welcome > .buttons {
|
.title .welcome > .buttons {
|
||||||
|
|
|
@ -15,11 +15,12 @@
|
||||||
import Spinner from "components/common/Spinner.svelte"
|
import Spinner from "components/common/Spinner.svelte"
|
||||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||||
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
||||||
|
import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte"
|
||||||
|
|
||||||
import { store, automationStore } from "builderStore"
|
import { store, automationStore } from "builderStore"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { apps, auth, admin, templates } from "stores/portal"
|
import { apps, auth, admin, templates, licensing } from "stores/portal"
|
||||||
import download from "downloadjs"
|
import download from "downloadjs"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import AppRow from "components/start/AppRow.svelte"
|
import AppRow from "components/start/AppRow.svelte"
|
||||||
|
@ -32,6 +33,7 @@
|
||||||
let selectedApp
|
let selectedApp
|
||||||
let creationModal
|
let creationModal
|
||||||
let updatingModal
|
let updatingModal
|
||||||
|
let appLimitModal
|
||||||
let creatingApp = false
|
let creatingApp = false
|
||||||
let loaded = $apps?.length || $templates?.length
|
let loaded = $apps?.length || $templates?.length
|
||||||
let searchTerm = ""
|
let searchTerm = ""
|
||||||
|
@ -124,8 +126,10 @@
|
||||||
return `${app.name} - Automation error (${errorCount(errors)})`
|
return `${app.name} - Automation error (${errorCount(errors)})`
|
||||||
}
|
}
|
||||||
|
|
||||||
const initiateAppCreation = () => {
|
const initiateAppCreation = async () => {
|
||||||
if ($apps?.length) {
|
if ($licensing.usageMetrics.apps >= 100) {
|
||||||
|
appLimitModal.show()
|
||||||
|
} else if ($apps?.length) {
|
||||||
$goto("/builder/portal/apps/create")
|
$goto("/builder/portal/apps/create")
|
||||||
} else {
|
} else {
|
||||||
template = null
|
template = null
|
||||||
|
@ -225,6 +229,10 @@
|
||||||
try {
|
try {
|
||||||
await apps.load()
|
await apps.load()
|
||||||
await templates.load()
|
await templates.load()
|
||||||
|
|
||||||
|
await licensing.getQuotaUsage()
|
||||||
|
await licensing.getUsageMetrics()
|
||||||
|
|
||||||
if ($templates?.length === 0) {
|
if ($templates?.length === 0) {
|
||||||
notifications.error(
|
notifications.error(
|
||||||
"There was a problem loading quick start templates."
|
"There was a problem loading quick start templates."
|
||||||
|
@ -405,6 +413,8 @@
|
||||||
<UpdateAppModal app={selectedApp} />
|
<UpdateAppModal app={selectedApp} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<AppLimitModal bind:this={appLimitModal} />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.appTable {
|
.appTable {
|
||||||
border-top: var(--border-light);
|
border-top: var(--border-light);
|
||||||
|
|
|
@ -38,7 +38,11 @@
|
||||||
try {
|
try {
|
||||||
await groups.actions.save(group)
|
await groups.actions.save(group)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error(`Failed to save group`)
|
if (error.status === 400) {
|
||||||
|
notifications.error(error.message)
|
||||||
|
} else {
|
||||||
|
notifications.error(`Failed to save group`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,10 +35,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const activate = async () => {
|
const activate = async () => {
|
||||||
await API.activateLicenseKey({ licenseKey })
|
try {
|
||||||
await auth.getSelf()
|
await API.activateLicenseKey({ licenseKey })
|
||||||
await setLicenseInfo()
|
await auth.getSelf()
|
||||||
notifications.success("Successfully activated")
|
await setLicenseInfo()
|
||||||
|
notifications.success("Successfully activated")
|
||||||
|
} catch (e) {
|
||||||
|
notifications.error(e.message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
|
|
|
@ -5,49 +5,74 @@
|
||||||
Heading,
|
Heading,
|
||||||
Layout,
|
Layout,
|
||||||
notifications,
|
notifications,
|
||||||
|
Detail,
|
||||||
Link,
|
Link,
|
||||||
|
TooltipWrapper,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { admin, auth, licensing } from "stores/portal"
|
import { admin, auth, licensing } from "../../../../stores/portal"
|
||||||
import Usage from "components/usage/Usage.svelte"
|
import { PlanType } from "../../../../constants"
|
||||||
|
import { DashCard, Usage } from "../../../../components/usage"
|
||||||
|
|
||||||
let staticUsage = []
|
let staticUsage = []
|
||||||
let monthlyUsage = []
|
let monthlyUsage = []
|
||||||
|
let cancelAt
|
||||||
let loaded = false
|
let loaded = false
|
||||||
|
let textRows = []
|
||||||
|
let daysRemainingInMonth
|
||||||
|
let primaryActionText
|
||||||
|
|
||||||
|
const upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade`
|
||||||
|
const manageUrl = `${$admin.accountPortalUrl}/portal/billing`
|
||||||
|
|
||||||
|
const WARN_USAGE = ["Queries", "Automations", "Rows", "Day Passes"]
|
||||||
|
const EXCLUDE_QUOTAS = ["Queries"]
|
||||||
|
|
||||||
$: quotaUsage = $licensing.quotaUsage
|
$: quotaUsage = $licensing.quotaUsage
|
||||||
$: license = $auth.user?.license
|
$: license = $auth.user?.license
|
||||||
|
$: accountPortalAccess = $auth?.user?.accountPortalAccess
|
||||||
const upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade`
|
$: quotaReset = quotaUsage?.quotaReset
|
||||||
|
|
||||||
const setMonthlyUsage = () => {
|
const setMonthlyUsage = () => {
|
||||||
monthlyUsage = []
|
monthlyUsage = []
|
||||||
if (quotaUsage.monthly) {
|
if (quotaUsage.monthly) {
|
||||||
for (let [key, value] of Object.entries(license.quotas.usage.monthly)) {
|
for (let [key, value] of Object.entries(license.quotas.usage.monthly)) {
|
||||||
|
if (EXCLUDE_QUOTAS.includes(value.name)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
const used = quotaUsage.monthly.current[key]
|
const used = quotaUsage.monthly.current[key]
|
||||||
if (used !== undefined) {
|
if (value.value !== 0) {
|
||||||
monthlyUsage.push({
|
monthlyUsage.push({
|
||||||
name: value.name,
|
name: value.name,
|
||||||
used: used,
|
used: used ? used : 0,
|
||||||
total: value.value,
|
total: value.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
monthlyUsage = monthlyUsage.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
const setStaticUsage = () => {
|
const setStaticUsage = () => {
|
||||||
staticUsage = []
|
staticUsage = []
|
||||||
for (let [key, value] of Object.entries(license.quotas.usage.static)) {
|
for (let [key, value] of Object.entries(license.quotas.usage.static)) {
|
||||||
|
if (EXCLUDE_QUOTAS.includes(value.name)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
const used = quotaUsage.usageQuota[key]
|
const used = quotaUsage.usageQuota[key]
|
||||||
if (used !== undefined) {
|
if (value.value !== 0) {
|
||||||
staticUsage.push({
|
staticUsage.push({
|
||||||
name: value.name,
|
name: value.name,
|
||||||
used: used,
|
used: used ? used : 0,
|
||||||
total: value.value,
|
total: value.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
staticUsage = staticUsage.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
const setCancelAt = () => {
|
||||||
|
cancelAt = license?.billing?.subscription?.cancelAt
|
||||||
}
|
}
|
||||||
|
|
||||||
const capitalise = string => {
|
const capitalise = string => {
|
||||||
|
@ -56,6 +81,70 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const planTitle = () => {
|
||||||
|
return capitalise(license?.plan.type)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDaysRemaining = timestamp => {
|
||||||
|
if (!timestamp) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const now = new Date()
|
||||||
|
now.setHours(0)
|
||||||
|
now.setMinutes(0)
|
||||||
|
|
||||||
|
const thenDate = new Date(timestamp)
|
||||||
|
thenDate.setHours(0)
|
||||||
|
thenDate.setMinutes(0)
|
||||||
|
|
||||||
|
const difference = thenDate.getTime() - now
|
||||||
|
// return the difference in days
|
||||||
|
return (difference / (1000 * 3600 * 24)).toFixed(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setTextRows = () => {
|
||||||
|
textRows = []
|
||||||
|
|
||||||
|
if (cancelAt) {
|
||||||
|
textRows.push({ message: "Subscription has been cancelled" })
|
||||||
|
textRows.push({
|
||||||
|
message: `${getDaysRemaining(cancelAt * 1000)} days remaining`,
|
||||||
|
tooltip: new Date(cancelAt * 1000),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setDaysRemainingInMonth = () => {
|
||||||
|
const resetDate = new Date(quotaReset)
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
|
const difference = resetDate.getTime() - now.getTime()
|
||||||
|
|
||||||
|
// return the difference in days
|
||||||
|
daysRemainingInMonth = (difference / (1000 * 3600 * 24)).toFixed(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToAccountPortal = () => {
|
||||||
|
if (license?.plan.type === PlanType.FREE) {
|
||||||
|
window.location.href = upgradeUrl
|
||||||
|
} else {
|
||||||
|
window.location.href = manageUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setPrimaryActionText = () => {
|
||||||
|
if (license?.plan.type === PlanType.FREE) {
|
||||||
|
primaryActionText = "Upgrade"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cancelAt) {
|
||||||
|
primaryActionText = "Renew"
|
||||||
|
} else {
|
||||||
|
primaryActionText = "Manage"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
try {
|
try {
|
||||||
await licensing.getQuotaUsage()
|
await licensing.getQuotaUsage()
|
||||||
|
@ -71,69 +160,98 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (license && quotaUsage) {
|
if (license) {
|
||||||
setMonthlyUsage()
|
setPrimaryActionText()
|
||||||
setStaticUsage()
|
setCancelAt()
|
||||||
|
setTextRows()
|
||||||
|
setDaysRemainingInMonth()
|
||||||
|
|
||||||
|
if (quotaUsage) {
|
||||||
|
setMonthlyUsage()
|
||||||
|
setStaticUsage()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
<Layout>
|
<Layout>
|
||||||
<Heading>Usage</Heading>
|
<Layout noPadding gap="S">
|
||||||
<Body
|
<Heading>Usage</Heading>
|
||||||
>Get information about your current usage within Budibase.
|
<Body
|
||||||
{#if $admin.cloud}
|
>Get information about your current usage within Budibase.
|
||||||
{#if $auth.user?.accountPortalAccess}
|
{#if accountPortalAccess}
|
||||||
To upgrade your plan and usage limits visit your <Link
|
To upgrade your plan and usage limits visit your <Link
|
||||||
size="L"
|
on:click={goToAccountPortal}
|
||||||
href={upgradeUrl}>Account</Link
|
size="L">Account</Link
|
||||||
>.
|
>
|
||||||
{:else}
|
{:else}
|
||||||
Contact your account holder to upgrade your usage limits.
|
To upgrade your plan and usage limits contact your account holder
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
</Body>
|
||||||
</Body>
|
|
||||||
</Layout>
|
|
||||||
<Layout gap="S">
|
|
||||||
<Divider size="S" />
|
|
||||||
</Layout>
|
|
||||||
<Layout gap="S" noPadding>
|
|
||||||
<Layout gap="XS">
|
|
||||||
<Body size="S">YOUR PLAN</Body>
|
|
||||||
<Heading size="S">{capitalise(license?.plan.type)}</Heading>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
<Layout gap="S">
|
<Divider />
|
||||||
<Body size="S">USAGE</Body>
|
<DashCard
|
||||||
<div class="usages">
|
description="YOUR CURRENT PLAN"
|
||||||
{#each staticUsage as usage}
|
title={planTitle()}
|
||||||
<div class="usage">
|
{primaryActionText}
|
||||||
<Usage {usage} />
|
primaryAction={accountPortalAccess ? goToAccountPortal : undefined}
|
||||||
|
{textRows}
|
||||||
|
>
|
||||||
|
<Layout gap="S" noPadding>
|
||||||
|
<Layout gap="S">
|
||||||
|
<div class="usages">
|
||||||
|
<Layout noPadding>
|
||||||
|
{#each staticUsage as usage}
|
||||||
|
<div class="usage">
|
||||||
|
<Usage
|
||||||
|
{usage}
|
||||||
|
warnWhenFull={WARN_USAGE.includes(usage.name)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
</Layout>
|
||||||
</div>
|
{#if monthlyUsage.length}
|
||||||
</Layout>
|
<div class="monthly-container">
|
||||||
{#if monthlyUsage.length}
|
<Layout gap="S">
|
||||||
<Layout gap="S">
|
<Heading size="S" weight="light">Monthly</Heading>
|
||||||
<Body size="S">MONTHLY</Body>
|
<div class="detail">
|
||||||
<div class="usages">
|
<TooltipWrapper tooltip={new Date(quotaReset)}>
|
||||||
{#each monthlyUsage as usage}
|
<Detail size="M">Resets in {daysRemainingInMonth} days</Detail
|
||||||
<div class="usage">
|
>
|
||||||
<Usage {usage} />
|
</TooltipWrapper>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
<div class="usages">
|
||||||
</div>
|
<Layout noPadding>
|
||||||
|
{#each monthlyUsage as usage}
|
||||||
|
<div class="usage">
|
||||||
|
<Usage
|
||||||
|
{usage}
|
||||||
|
warnWhenFull={WARN_USAGE.includes(usage.name)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</Layout>
|
</Layout>
|
||||||
<div />
|
</DashCard>
|
||||||
{/if}
|
|
||||||
</Layout>
|
</Layout>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.usages {
|
.usages {
|
||||||
display: grid;
|
display: flex;
|
||||||
column-gap: 60px;
|
flex-direction: column;
|
||||||
row-gap: 50px;
|
}
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
.detail :global(.spectrum-Detail) {
|
||||||
|
color: var(--spectrum-global-color-gray-700);
|
||||||
|
}
|
||||||
|
.detail :global(.icon) {
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,14 +1,22 @@
|
||||||
import { writable } from "svelte/store"
|
import { writable, get } from "svelte/store"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
import { auth } from "stores/portal"
|
||||||
|
import { Constants } from "@budibase/frontend-core"
|
||||||
|
import { StripeStatus } from "components/portal/licensing/constants"
|
||||||
|
|
||||||
export const createLicensingStore = () => {
|
export const createLicensingStore = () => {
|
||||||
const DEFAULT = {
|
const DEFAULT = {
|
||||||
plans: {},
|
plans: {},
|
||||||
}
|
}
|
||||||
|
const oneDayInMilliseconds = 86400000
|
||||||
|
|
||||||
const store = writable(DEFAULT)
|
const store = writable(DEFAULT)
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
|
init: async () => {
|
||||||
|
await actions.getQuotaUsage()
|
||||||
|
await actions.getUsageMetrics()
|
||||||
|
},
|
||||||
getQuotaUsage: async () => {
|
getQuotaUsage: async () => {
|
||||||
const quotaUsage = await API.getQuotaUsage()
|
const quotaUsage = await API.getQuotaUsage()
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
|
@ -18,6 +26,80 @@ export const createLicensingStore = () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
getUsageMetrics: async () => {
|
||||||
|
const quota = get(store).quotaUsage
|
||||||
|
const license = get(auth).user.license
|
||||||
|
const now = new Date()
|
||||||
|
|
||||||
|
const getMetrics = (keys, license, quota) => {
|
||||||
|
if (!license || !quota || !keys) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
return keys.reduce((acc, key) => {
|
||||||
|
const quotaLimit = license[key].value
|
||||||
|
const quotaUsed = (quota[key] / quotaLimit) * 100
|
||||||
|
acc[key] = quotaLimit > -1 ? Math.round(quotaUsed) : -1
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
const monthlyMetrics = getMetrics(
|
||||||
|
["dayPasses", "queries", "automations"],
|
||||||
|
license.quotas.usage.monthly,
|
||||||
|
quota.monthly.current
|
||||||
|
)
|
||||||
|
const staticMetrics = getMetrics(
|
||||||
|
["apps", "rows"],
|
||||||
|
license.quotas.usage.static,
|
||||||
|
quota.usageQuota
|
||||||
|
)
|
||||||
|
|
||||||
|
const getDaysBetween = (dateStart, dateEnd) => {
|
||||||
|
return dateEnd > dateStart
|
||||||
|
? Math.round(
|
||||||
|
(dateEnd.getTime() - dateStart.getTime()) / oneDayInMilliseconds
|
||||||
|
)
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const quotaResetDate = new Date(quota.quotaReset)
|
||||||
|
const quotaResetDaysRemaining = getDaysBetween(now, quotaResetDate)
|
||||||
|
|
||||||
|
const accountDowngraded =
|
||||||
|
license?.billing?.subscription?.downgradeAt &&
|
||||||
|
license?.billing?.subscription?.downgradeAt <= now.getTime() &&
|
||||||
|
license?.billing?.subscription?.status === StripeStatus.PAST_DUE &&
|
||||||
|
license?.plan.type === Constants.PlanType.FREE
|
||||||
|
|
||||||
|
const pastDueAtMilliseconds = license?.billing?.subscription?.pastDueAt
|
||||||
|
const downgradeAtMilliseconds =
|
||||||
|
license?.billing?.subscription?.downgradeAt
|
||||||
|
let pastDueDaysRemaining
|
||||||
|
let pastDueEndDate
|
||||||
|
|
||||||
|
if (pastDueAtMilliseconds && downgradeAtMilliseconds) {
|
||||||
|
pastDueEndDate = new Date(downgradeAtMilliseconds)
|
||||||
|
pastDueDaysRemaining = getDaysBetween(
|
||||||
|
new Date(pastDueAtMilliseconds),
|
||||||
|
pastDueEndDate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
store.update(state => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
usageMetrics: { ...monthlyMetrics, ...staticMetrics },
|
||||||
|
quotaResetDaysRemaining,
|
||||||
|
quotaResetDate,
|
||||||
|
accountDowngraded,
|
||||||
|
accountPastDue: pastDueAtMilliseconds != null,
|
||||||
|
pastDueEndDate,
|
||||||
|
pastDueDaysRemaining,
|
||||||
|
isFreePlan: () => {
|
||||||
|
return license?.plan.type === Constants.PlanType.FREE
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,15 +1,20 @@
|
||||||
<a
|
<script>
|
||||||
|
import { Link } from "@budibase/bbui"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Link
|
||||||
href="https://www.budibase.com/?utm_source=budibase-apps-public-screens&utm_medium=badge&utm_campaign=made-in-budibase"
|
href="https://www.budibase.com/?utm_source=budibase-apps-public-screens&utm_medium=badge&utm_campaign=made-in-budibase"
|
||||||
|
target="_blank"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<img src="https://i.imgur.com/Xhdt1YP.png" alt="Budibase" />
|
<img src="https://i.imgur.com/Xhdt1YP.png" alt="Budibase" />
|
||||||
<p>Made In Budibase</p>
|
<p>Made In Budibase</p>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</Link>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
div {
|
div {
|
||||||
position: absolute;
|
position: fixed;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
@ -27,12 +32,7 @@
|
||||||
|
|
||||||
p {
|
p {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--spectrum-heading-m-text-color);
|
color: var(--spectrum-global-color-gray-900);
|
||||||
}
|
|
||||||
|
|
||||||
a:visited {
|
|
||||||
text-decoration: none;
|
|
||||||
color: var(--spectrum-heading-m-text-color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
|
|
|
@ -5,9 +5,18 @@
|
||||||
import { FieldTypes } from "constants"
|
import { FieldTypes } from "constants"
|
||||||
import active from "svelte-spa-router/active"
|
import active from "svelte-spa-router/active"
|
||||||
import { RoleUtils } from "@budibase/frontend-core"
|
import { RoleUtils } from "@budibase/frontend-core"
|
||||||
|
import MadeInBudibase from "../MadeInBudibase.svelte"
|
||||||
|
import licensing from "../../licensing"
|
||||||
|
|
||||||
const sdk = getContext("sdk")
|
const sdk = getContext("sdk")
|
||||||
const { routeStore, styleable, linkable, builderStore, currentRole } = sdk
|
const {
|
||||||
|
routeStore,
|
||||||
|
styleable,
|
||||||
|
linkable,
|
||||||
|
builderStore,
|
||||||
|
currentRole,
|
||||||
|
environmentStore,
|
||||||
|
} = sdk
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
const context = getContext("context")
|
const context = getContext("context")
|
||||||
|
|
||||||
|
@ -225,6 +234,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if !$builderStore.inBuilder && licensing.logoEnabled() && $environmentStore.cloud}
|
||||||
|
<MadeInBudibase />
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="main-wrapper">
|
<div class="main-wrapper">
|
||||||
<div class="main size--{pageWidthClass}">
|
<div class="main size--{pageWidthClass}">
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
export const PlanType = {
|
||||||
|
FREE: "free",
|
||||||
|
TEAM: "team",
|
||||||
|
BUSINESS: "business",
|
||||||
|
ENTERPRISE: "enterprise",
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { isFreePlan } from "./utils.js"
|
||||||
|
|
||||||
|
export const logoEnabled = () => {
|
||||||
|
return isFreePlan()
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import * as features from "./features"
|
||||||
|
|
||||||
|
const licensing = {
|
||||||
|
...features,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default licensing
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { authStore } from "../stores/auth.js"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import { PlanType } from "./constants"
|
||||||
|
|
||||||
|
const getLicense = () => {
|
||||||
|
const user = get(authStore)
|
||||||
|
if (user) {
|
||||||
|
return user.license
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isFreePlan = () => {
|
||||||
|
const license = getLicense()
|
||||||
|
if (license) {
|
||||||
|
return license.plan.type === PlanType.FREE
|
||||||
|
} else {
|
||||||
|
// safety net - no license means free plan
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import {
|
||||||
rowSelectionStore,
|
rowSelectionStore,
|
||||||
componentStore,
|
componentStore,
|
||||||
currentRole,
|
currentRole,
|
||||||
|
environmentStore,
|
||||||
} from "stores"
|
} from "stores"
|
||||||
import { styleable } from "utils/styleable"
|
import { styleable } from "utils/styleable"
|
||||||
import { linkable } from "utils/linkable"
|
import { linkable } from "utils/linkable"
|
||||||
|
@ -27,6 +28,7 @@ export default {
|
||||||
builderStore,
|
builderStore,
|
||||||
uploadStore,
|
uploadStore,
|
||||||
componentStore,
|
componentStore,
|
||||||
|
environmentStore,
|
||||||
currentRole,
|
currentRole,
|
||||||
styleable,
|
styleable,
|
||||||
linkable,
|
linkable,
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { API } from "api"
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
cloud: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
const createEnvironmentStore = () => {
|
||||||
|
const store = writable(initialState)
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
fetchEnvironment: async () => {
|
||||||
|
try {
|
||||||
|
const environment = await API.getEnvironment()
|
||||||
|
store.set({
|
||||||
|
...initialState,
|
||||||
|
...environment,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
store.set(initialState)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: store.subscribe,
|
||||||
|
actions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const environmentStore = createEnvironmentStore()
|
|
@ -17,6 +17,7 @@ export { devToolsStore } from "./devTools"
|
||||||
export { componentStore } from "./components"
|
export { componentStore } from "./components"
|
||||||
export { uploadStore } from "./uploads.js"
|
export { uploadStore } from "./uploads.js"
|
||||||
export { rowSelectionStore } from "./rowSelection.js"
|
export { rowSelectionStore } from "./rowSelection.js"
|
||||||
|
export { environmentStore } from "./environment"
|
||||||
|
|
||||||
// Context stores are layered and duplicated, so it is not a singleton
|
// Context stores are layered and duplicated, so it is not a singleton
|
||||||
export { createContextStore } from "./context"
|
export { createContextStore } from "./context"
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import { routeStore } from "./routes"
|
import { routeStore } from "./routes"
|
||||||
import { appStore } from "./app"
|
import { appStore } from "./app"
|
||||||
|
import { environmentStore } from "./environment"
|
||||||
|
|
||||||
export async function initialise() {
|
export async function initialise() {
|
||||||
await routeStore.actions.fetchRoutes()
|
await routeStore.actions.fetchRoutes()
|
||||||
await appStore.actions.fetchAppDefinition()
|
await appStore.actions.fetchAppDefinition()
|
||||||
|
await environmentStore.actions.fetchEnvironment()
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,6 +57,7 @@ export const Cookies = {
|
||||||
Auth: "budibase:auth",
|
Auth: "budibase:auth",
|
||||||
CurrentApp: "budibase:currentapp",
|
CurrentApp: "budibase:currentapp",
|
||||||
ReturnUrl: "budibase:returnurl",
|
ReturnUrl: "budibase:returnurl",
|
||||||
|
AccountReturnUrl: "budibase:account:returnurl",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Table names
|
// Table names
|
||||||
|
|
|
@ -17,6 +17,8 @@ const arg = process.argv.slice(2)[0]
|
||||||
*/
|
*/
|
||||||
updateDotEnv({
|
updateDotEnv({
|
||||||
ACCOUNT_PORTAL_URL:
|
ACCOUNT_PORTAL_URL:
|
||||||
arg === "enable" ? "http://local.com:10001" : "http://localhost:10001",
|
arg === "enable"
|
||||||
|
? "http://account.local.com:10001"
|
||||||
|
: "http://localhost:10001",
|
||||||
COOKIE_DOMAIN: arg === "enable" ? ".local.com" : "",
|
COOKIE_DOMAIN: arg === "enable" ? ".local.com" : "",
|
||||||
}).then(() => console.log("Updated worker!"))
|
}).then(() => console.log("Updated worker!"))
|
||||||
|
|
|
@ -472,7 +472,6 @@ const destroyApp = async (ctx: any) => {
|
||||||
const result = await db.destroy()
|
const result = await db.destroy()
|
||||||
|
|
||||||
if (isUnpublish) {
|
if (isUnpublish) {
|
||||||
await quotas.removePublishedApp()
|
|
||||||
await events.app.unpublished(app)
|
await events.app.unpublished(app)
|
||||||
} else {
|
} else {
|
||||||
await quotas.removeApp()
|
await quotas.removeApp()
|
||||||
|
|
|
@ -4,6 +4,15 @@ const { getFullUser } = require("../../utilities/users")
|
||||||
const { BUILTIN_ROLE_IDS } = require("@budibase/backend-core/roles")
|
const { BUILTIN_ROLE_IDS } = require("@budibase/backend-core/roles")
|
||||||
const { getAppDB, getAppId } = require("@budibase/backend-core/context")
|
const { getAppDB, getAppId } = require("@budibase/backend-core/context")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the attributes that are session based to the current user.
|
||||||
|
*/
|
||||||
|
const addSessionAttributesToUser = ctx => {
|
||||||
|
if (ctx.user) {
|
||||||
|
ctx.body.license = ctx.user.license
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
exports.fetchSelf = async ctx => {
|
exports.fetchSelf = async ctx => {
|
||||||
let userId = ctx.user.userId || ctx.user._id
|
let userId = ctx.user.userId || ctx.user._id
|
||||||
/* istanbul ignore next */
|
/* istanbul ignore next */
|
||||||
|
@ -55,4 +64,6 @@ exports.fetchSelf = async ctx => {
|
||||||
} else {
|
} else {
|
||||||
ctx.body = user
|
ctx.body = user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addSessionAttributesToUser(ctx)
|
||||||
}
|
}
|
||||||
|
|
|
@ -206,12 +206,7 @@ const _deployApp = async function (ctx: any) {
|
||||||
|
|
||||||
console.log("Deploying app...")
|
console.log("Deploying app...")
|
||||||
|
|
||||||
let app
|
let app = await deployApp(deployment)
|
||||||
if (await isFirstDeploy()) {
|
|
||||||
app = await quotas.addPublishedApp(() => deployApp(deployment))
|
|
||||||
} else {
|
|
||||||
app = await deployApp(deployment)
|
|
||||||
}
|
|
||||||
|
|
||||||
await events.app.published(app)
|
await events.app.published(app)
|
||||||
ctx.body = deployment
|
ctx.body = deployment
|
||||||
|
|
|
@ -18,7 +18,6 @@ const { DocumentType } = require("../../../db/utils")
|
||||||
const { getAppDB, getAppId } = require("@budibase/backend-core/context")
|
const { getAppDB, getAppId } = require("@budibase/backend-core/context")
|
||||||
const { setCookie, clearCookie } = require("@budibase/backend-core/utils")
|
const { setCookie, clearCookie } = require("@budibase/backend-core/utils")
|
||||||
const AWS = require("aws-sdk")
|
const AWS = require("aws-sdk")
|
||||||
|
|
||||||
const fs = require("fs")
|
const fs = require("fs")
|
||||||
const {
|
const {
|
||||||
downloadTarballDirect,
|
downloadTarballDirect,
|
||||||
|
|
|
@ -54,8 +54,8 @@ router
|
||||||
noTenancyRequired: true,
|
noTenancyRequired: true,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.use(currentApp)
|
|
||||||
.use(pro.licensing())
|
.use(pro.licensing())
|
||||||
|
.use(currentApp)
|
||||||
.use(auditLog)
|
.use(auditLog)
|
||||||
|
|
||||||
// error handling middleware
|
// error handling middleware
|
||||||
|
|
|
@ -23,7 +23,6 @@ describe("/users", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("fetch", () => {
|
describe("fetch", () => {
|
||||||
|
|
||||||
it("returns a list of users from an instance db", async () => {
|
it("returns a list of users from an instance db", async () => {
|
||||||
await config.createUser("uuidx")
|
await config.createUser("uuidx")
|
||||||
await config.createUser("uuidy")
|
await config.createUser("uuidy")
|
||||||
|
@ -38,7 +37,6 @@ describe("/users", () => {
|
||||||
expect(res.body.find(u => u._id === `ro_ta_users_us_uuidy`)).toBeDefined()
|
expect(res.body.find(u => u._id === `ro_ta_users_us_uuidy`)).toBeDefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
it("should apply authorization to endpoint", async () => {
|
it("should apply authorization to endpoint", async () => {
|
||||||
await config.createUser()
|
await config.createUser()
|
||||||
await checkPermissionsEndpoint({
|
await checkPermissionsEndpoint({
|
||||||
|
|
|
@ -111,12 +111,14 @@ module.exports = async (ctx, next) => {
|
||||||
ctx.appId = appId
|
ctx.appId = appId
|
||||||
if (roleId) {
|
if (roleId) {
|
||||||
ctx.roleId = roleId
|
ctx.roleId = roleId
|
||||||
|
const globalId = ctx.user ? ctx.user._id : undefined
|
||||||
const userId = ctx.user ? generateUserMetadataID(ctx.user._id) : null
|
const userId = ctx.user ? generateUserMetadataID(ctx.user._id) : null
|
||||||
ctx.user = {
|
ctx.user = {
|
||||||
...ctx.user,
|
...ctx.user,
|
||||||
// override userID with metadata one
|
// override userID with metadata one
|
||||||
_id: userId,
|
_id: userId,
|
||||||
userId,
|
userId,
|
||||||
|
globalId,
|
||||||
roleId,
|
roleId,
|
||||||
role: await getRole(roleId),
|
role: await getRole(roleId),
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,12 +35,10 @@ const formatUsage = (usage: QuotaUsage) => {
|
||||||
let maxAutomations = 0
|
let maxAutomations = 0
|
||||||
let maxQueries = 0
|
let maxQueries = 0
|
||||||
let rows = 0
|
let rows = 0
|
||||||
let developers = 0
|
|
||||||
|
|
||||||
if (usage) {
|
if (usage) {
|
||||||
if (usage.usageQuota) {
|
if (usage.usageQuota) {
|
||||||
rows = usage.usageQuota.rows
|
rows = usage.usageQuota.rows
|
||||||
developers = usage.usageQuota.developers
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (usage.monthly) {
|
if (usage.monthly) {
|
||||||
|
@ -59,7 +57,6 @@ const formatUsage = (usage: QuotaUsage) => {
|
||||||
maxAutomations,
|
maxAutomations,
|
||||||
maxQueries,
|
maxQueries,
|
||||||
rows,
|
rows,
|
||||||
developers,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
const { createUserBuildersView } = require("@budibase/backend-core/db")
|
|
||||||
import * as syncDevelopers from "./usageQuotas/syncDevelopers"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Date:
|
|
||||||
* March 2022
|
|
||||||
*
|
|
||||||
* Description:
|
|
||||||
* Create the builder users view and sync the developer count
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const run = async (db: any) => {
|
|
||||||
await createUserBuildersView(db)
|
|
||||||
await syncDevelopers.run()
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
import * as syncPublishedApps from "./usageQuotas/syncPublishedApps"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Date:
|
|
||||||
* March 2022
|
|
||||||
*
|
|
||||||
* Description:
|
|
||||||
* Sync the published apps count
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const run = async (db: any) => {
|
|
||||||
await syncPublishedApps.run()
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
import { getTenantId } from "@budibase/backend-core/tenancy"
|
|
||||||
import { utils } from "@budibase/backend-core"
|
|
||||||
import { quotas, QuotaUsageType, StaticQuotaName } from "@budibase/pro"
|
|
||||||
|
|
||||||
export const run = async () => {
|
|
||||||
// get developer count
|
|
||||||
const developerCount = await utils.getBuildersCount()
|
|
||||||
|
|
||||||
// sync developer count
|
|
||||||
const tenantId = getTenantId()
|
|
||||||
console.log(
|
|
||||||
`[Tenant: ${tenantId}] Syncing developer count: ${developerCount}`
|
|
||||||
)
|
|
||||||
await quotas.setUsage(
|
|
||||||
developerCount,
|
|
||||||
StaticQuotaName.DEVELOPERS,
|
|
||||||
QuotaUsageType.STATIC
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
import { getTenantId } from "@budibase/backend-core/tenancy"
|
|
||||||
import { getAllApps } from "@budibase/backend-core/db"
|
|
||||||
import { quotas, QuotaUsageType, StaticQuotaName } from "@budibase/pro"
|
|
||||||
|
|
||||||
export const run = async () => {
|
|
||||||
// get app count
|
|
||||||
const opts: any = { dev: false }
|
|
||||||
const prodApps = await getAllApps(opts)
|
|
||||||
const prodAppCount = prodApps ? prodApps.length : 0
|
|
||||||
|
|
||||||
// sync app count
|
|
||||||
const tenantId = getTenantId()
|
|
||||||
console.log(
|
|
||||||
`[Tenant: ${tenantId}] Syncing published app count: ${prodAppCount}`
|
|
||||||
)
|
|
||||||
await quotas.setUsage(
|
|
||||||
prodAppCount,
|
|
||||||
StaticQuotaName.PUBLISHED_APPS,
|
|
||||||
QuotaUsageType.STATIC
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -6,8 +6,6 @@ import env from "../environment"
|
||||||
import * as userEmailViewCasing from "./functions/userEmailViewCasing"
|
import * as userEmailViewCasing from "./functions/userEmailViewCasing"
|
||||||
import * as quota1 from "./functions/quotas1"
|
import * as quota1 from "./functions/quotas1"
|
||||||
import * as appUrls from "./functions/appUrls"
|
import * as appUrls from "./functions/appUrls"
|
||||||
import * as developerQuota from "./functions/developerQuota"
|
|
||||||
import * as publishedAppsQuota from "./functions/publishedAppsQuota"
|
|
||||||
import * as backfill from "./functions/backfill"
|
import * as backfill from "./functions/backfill"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -42,20 +40,6 @@ export const buildMigrations = () => {
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case MigrationName.DEVELOPER_QUOTA: {
|
|
||||||
serverMigrations.push({
|
|
||||||
...definition,
|
|
||||||
fn: developerQuota.run,
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case MigrationName.PUBLISHED_APP_QUOTA: {
|
|
||||||
serverMigrations.push({
|
|
||||||
...definition,
|
|
||||||
fn: publishedAppsQuota.run,
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case MigrationName.EVENT_APP_BACKFILL: {
|
case MigrationName.EVENT_APP_BACKFILL: {
|
||||||
serverMigrations.push({
|
serverMigrations.push({
|
||||||
...definition,
|
...definition,
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
"jest": {},
|
"jest": {},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "14.18.20",
|
"@types/node": "14.18.20",
|
||||||
|
"@types/koa": "2.13.4",
|
||||||
"rimraf": "3.0.2",
|
"rimraf": "3.0.2",
|
||||||
"typescript": "4.7.3"
|
"typescript": "4.7.3"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./user"
|
|
@ -0,0 +1,8 @@
|
||||||
|
export interface PostAccountUserActivity {
|
||||||
|
timestamp: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PostAccountUserActivityResponse {
|
||||||
|
userId: string
|
||||||
|
timestamp: number
|
||||||
|
}
|
|
@ -1 +1,2 @@
|
||||||
|
export * from "./account"
|
||||||
export * from "./web"
|
export * from "./web"
|
||||||
|
|
|
@ -22,11 +22,6 @@ export const isCreatePasswordAccount = (
|
||||||
account: CreateAccount
|
account: CreateAccount
|
||||||
): account is CreatePassswordAccount => account.authType === AuthType.PASSWORD
|
): account is CreatePassswordAccount => account.authType === AuthType.PASSWORD
|
||||||
|
|
||||||
export interface UpdateAccount {
|
|
||||||
stripeCustomerId?: string
|
|
||||||
licenseKey?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Account extends CreateAccount {
|
export interface Account extends CreateAccount {
|
||||||
// generated
|
// generated
|
||||||
accountId: string
|
accountId: string
|
||||||
|
@ -38,6 +33,7 @@ export interface Account extends CreateAccount {
|
||||||
tier: string // deprecated
|
tier: string // deprecated
|
||||||
stripeCustomerId?: string
|
stripeCustomerId?: string
|
||||||
licenseKey?: string
|
licenseKey?: string
|
||||||
|
licenseKeyActivatedAt?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PasswordAccount extends Account {
|
export interface PasswordAccount extends Account {
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
export * from "./account"
|
export * from "./account"
|
||||||
|
export * from "./user"
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
export interface CreateAccountUserActivity {
|
||||||
|
accountId: string
|
||||||
|
userId: string
|
||||||
|
timestamp: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccountUserActivity extends CreateAccountUserActivity {
|
||||||
|
PK: string
|
||||||
|
SK: string
|
||||||
|
}
|
|
@ -16,6 +16,7 @@ export interface User extends Document {
|
||||||
createdAt?: number // override the default createdAt behaviour - users sdk historically set this to Date.now()
|
createdAt?: number // override the default createdAt behaviour - users sdk historically set this to Date.now()
|
||||||
userGroups?: string[]
|
userGroups?: string[]
|
||||||
forceResetPassword?: boolean
|
forceResetPassword?: boolean
|
||||||
|
dayPassRecordedAt?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserRoles {
|
export interface UserRoles {
|
||||||
|
|
|
@ -3,3 +3,25 @@ export interface AuthToken {
|
||||||
tenantId: string
|
tenantId: string
|
||||||
sessionId: string
|
sessionId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CreateSession {
|
||||||
|
sessionId: string
|
||||||
|
tenantId: string
|
||||||
|
csrfToken?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Session extends CreateSession {
|
||||||
|
userId: string
|
||||||
|
lastAccessedAt: string
|
||||||
|
createdAt: string
|
||||||
|
// make optional attributes required
|
||||||
|
csrfToken: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionKey {
|
||||||
|
key: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScannedSession {
|
||||||
|
value: Session
|
||||||
|
}
|
||||||
|
|
|
@ -5,4 +5,5 @@ export * from "./licensing"
|
||||||
export * from "./migrations"
|
export * from "./migrations"
|
||||||
export * from "./datasources"
|
export * from "./datasources"
|
||||||
export * from "./search"
|
export * from "./search"
|
||||||
|
export * from "./koa"
|
||||||
export * from "./auth"
|
export * from "./auth"
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { Context } from "koa"
|
||||||
|
import { User } from "../documents"
|
||||||
|
import { License } from "../sdk"
|
||||||
|
|
||||||
|
export interface ContextUser extends User {
|
||||||
|
globalId?: string
|
||||||
|
license: License
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BBContext extends Context {
|
||||||
|
user?: ContextUser
|
||||||
|
body: any
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { PriceDuration } from "./plan"
|
||||||
|
|
||||||
|
export interface Customer {
|
||||||
|
balance: number | null | undefined
|
||||||
|
currency: string | null | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Subscription {
|
||||||
|
amount: number
|
||||||
|
quantity: number
|
||||||
|
duration: PriceDuration
|
||||||
|
cancelAt: number | null | undefined
|
||||||
|
currentPeriodStart: number
|
||||||
|
currentPeriodEnd: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Billing {
|
||||||
|
customer: Customer
|
||||||
|
subscription?: Subscription
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export enum Feature {
|
||||||
|
USER_GROUPS = "userGroups",
|
||||||
|
}
|
|
@ -1 +1,5 @@
|
||||||
export * from "./license"
|
export * from "./license"
|
||||||
|
export * from "./plan"
|
||||||
|
export * from "./quota"
|
||||||
|
export * from "./feature"
|
||||||
|
export * from "./billing"
|
||||||
|
|
|
@ -1 +1,8 @@
|
||||||
export interface License {}
|
import { AccountPlan, Quotas, Feature, Billing } from "."
|
||||||
|
|
||||||
|
export interface License {
|
||||||
|
features: Feature[]
|
||||||
|
quotas: Quotas
|
||||||
|
plan: AccountPlan
|
||||||
|
billing?: Billing
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
export interface AccountPlan {
|
||||||
|
type: PlanType
|
||||||
|
price?: Price
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PlanType {
|
||||||
|
FREE = "free",
|
||||||
|
PRO = "pro",
|
||||||
|
BUSINESS = "business",
|
||||||
|
ENTERPRISE = "enterprise",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PriceDuration {
|
||||||
|
MONTHLY = "monthly",
|
||||||
|
YEARLY = "yearly",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Price {
|
||||||
|
amount: number
|
||||||
|
amountMonthly: number
|
||||||
|
currency: string
|
||||||
|
duration: PriceDuration
|
||||||
|
priceId: string
|
||||||
|
dayPasses: number
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { PlanType } from "."
|
||||||
|
|
||||||
|
export enum QuotaUsageType {
|
||||||
|
STATIC = "static",
|
||||||
|
MONTHLY = "monthly",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum QuotaType {
|
||||||
|
USAGE = "usage",
|
||||||
|
CONSTANT = "constant",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum StaticQuotaName {
|
||||||
|
ROWS = "rows",
|
||||||
|
APPS = "apps",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MonthlyQuotaName {
|
||||||
|
QUERIES = "queries",
|
||||||
|
AUTOMATIONS = "automations",
|
||||||
|
DAY_PASSES = "dayPasses",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ConstantQuotaName {
|
||||||
|
QUERY_TIMEOUT_SECONDS = "queryTimeoutSeconds",
|
||||||
|
AUTOMATION_LOG_RETENTION_DAYS = "automationLogRetentionDays",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QuotaName = StaticQuotaName | MonthlyQuotaName | ConstantQuotaName
|
||||||
|
|
||||||
|
export const isStaticQuota = (
|
||||||
|
quotaType: QuotaType,
|
||||||
|
usageType: QuotaUsageType,
|
||||||
|
name: QuotaName
|
||||||
|
): name is StaticQuotaName => {
|
||||||
|
return quotaType === QuotaType.USAGE && usageType === QuotaUsageType.STATIC
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isMonthlyQuota = (
|
||||||
|
quotaType: QuotaType,
|
||||||
|
usageType: QuotaUsageType,
|
||||||
|
name: QuotaName
|
||||||
|
): name is MonthlyQuotaName => {
|
||||||
|
return quotaType === QuotaType.USAGE && usageType === QuotaUsageType.MONTHLY
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isConstantQuota = (
|
||||||
|
quotaType: QuotaType,
|
||||||
|
name: QuotaName
|
||||||
|
): name is ConstantQuotaName => {
|
||||||
|
return quotaType === QuotaType.CONSTANT
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PlanQuotas = {
|
||||||
|
[PlanType.FREE]: Quotas
|
||||||
|
[PlanType.PRO]: Quotas
|
||||||
|
[PlanType.BUSINESS]: Quotas
|
||||||
|
[PlanType.ENTERPRISE]: Quotas
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Quotas = {
|
||||||
|
[QuotaType.USAGE]: {
|
||||||
|
[QuotaUsageType.MONTHLY]: {
|
||||||
|
[MonthlyQuotaName.QUERIES]: Quota
|
||||||
|
[MonthlyQuotaName.AUTOMATIONS]: Quota
|
||||||
|
[MonthlyQuotaName.DAY_PASSES]: Quota
|
||||||
|
}
|
||||||
|
[QuotaUsageType.STATIC]: {
|
||||||
|
[StaticQuotaName.ROWS]: Quota
|
||||||
|
[StaticQuotaName.APPS]: Quota
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[QuotaType.CONSTANT]: {
|
||||||
|
[ConstantQuotaName.QUERY_TIMEOUT_SECONDS]: Quota
|
||||||
|
[ConstantQuotaName.AUTOMATION_LOG_RETENTION_DAYS]: Quota
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Quota {
|
||||||
|
name: string
|
||||||
|
value: number
|
||||||
|
}
|
|
@ -41,8 +41,6 @@ export enum MigrationName {
|
||||||
USER_EMAIL_VIEW_CASING = "user_email_view_casing",
|
USER_EMAIL_VIEW_CASING = "user_email_view_casing",
|
||||||
QUOTAS_1 = "quotas_1",
|
QUOTAS_1 = "quotas_1",
|
||||||
APP_URLS = "app_urls",
|
APP_URLS = "app_urls",
|
||||||
DEVELOPER_QUOTA = "developer_quota",
|
|
||||||
PUBLISHED_APP_QUOTA = "published_apps_quota",
|
|
||||||
EVENT_APP_BACKFILL = "event_app_backfill",
|
EVENT_APP_BACKFILL = "event_app_backfill",
|
||||||
EVENT_GLOBAL_BACKFILL = "event_global_backfill",
|
EVENT_GLOBAL_BACKFILL = "event_global_backfill",
|
||||||
EVENT_INSTALLATION_BACKFILL = "event_installation_backfill",
|
EVENT_INSTALLATION_BACKFILL = "event_installation_backfill",
|
||||||
|
|
|
@ -2,11 +2,145 @@
|
||||||
# yarn lockfile v1
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
"@types/accepts@*":
|
||||||
|
version "1.3.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.5.tgz#c34bec115cfc746e04fe5a059df4ce7e7b391575"
|
||||||
|
integrity sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/body-parser@*":
|
||||||
|
version "1.19.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0"
|
||||||
|
integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==
|
||||||
|
dependencies:
|
||||||
|
"@types/connect" "*"
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/connect@*":
|
||||||
|
version "3.4.35"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1"
|
||||||
|
integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/content-disposition@*":
|
||||||
|
version "0.5.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/content-disposition/-/content-disposition-0.5.5.tgz#650820e95de346e1f84e30667d168c8fd25aa6e3"
|
||||||
|
integrity sha512-v6LCdKfK6BwcqMo+wYW05rLS12S0ZO0Fl4w1h4aaZMD7bqT3gVUns6FvLJKGZHQmYn3SX55JWGpziwJRwVgutA==
|
||||||
|
|
||||||
|
"@types/cookies@*":
|
||||||
|
version "0.7.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/cookies/-/cookies-0.7.7.tgz#7a92453d1d16389c05a5301eef566f34946cfd81"
|
||||||
|
integrity sha512-h7BcvPUogWbKCzBR2lY4oqaZbO3jXZksexYJVFvkrFeLgbZjQkU4x8pRq6eg2MHXQhY0McQdqmmsxRWlVAHooA==
|
||||||
|
dependencies:
|
||||||
|
"@types/connect" "*"
|
||||||
|
"@types/express" "*"
|
||||||
|
"@types/keygrip" "*"
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/express-serve-static-core@^4.17.18":
|
||||||
|
version "4.17.29"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.29.tgz#2a1795ea8e9e9c91b4a4bbe475034b20c1ec711c"
|
||||||
|
integrity sha512-uMd++6dMKS32EOuw1Uli3e3BPgdLIXmezcfHv7N4c1s3gkhikBplORPpMq3fuWkxncZN1reb16d5n8yhQ80x7Q==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
"@types/qs" "*"
|
||||||
|
"@types/range-parser" "*"
|
||||||
|
|
||||||
|
"@types/express@*":
|
||||||
|
version "4.17.13"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034"
|
||||||
|
integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==
|
||||||
|
dependencies:
|
||||||
|
"@types/body-parser" "*"
|
||||||
|
"@types/express-serve-static-core" "^4.17.18"
|
||||||
|
"@types/qs" "*"
|
||||||
|
"@types/serve-static" "*"
|
||||||
|
|
||||||
|
"@types/http-assert@*":
|
||||||
|
version "1.5.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/http-assert/-/http-assert-1.5.3.tgz#ef8e3d1a8d46c387f04ab0f2e8ab8cb0c5078661"
|
||||||
|
integrity sha512-FyAOrDuQmBi8/or3ns4rwPno7/9tJTijVW6aQQjK02+kOQ8zmoNg2XJtAuQhvQcy1ASJq38wirX5//9J1EqoUA==
|
||||||
|
|
||||||
|
"@types/http-errors@*":
|
||||||
|
version "1.8.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-1.8.2.tgz#7315b4c4c54f82d13fa61c228ec5c2ea5cc9e0e1"
|
||||||
|
integrity sha512-EqX+YQxINb+MeXaIqYDASb6U6FCHbWjkj4a1CKDBks3d/QiB2+PqBLyO72vLDgAO1wUI4O+9gweRcQK11bTL/w==
|
||||||
|
|
||||||
|
"@types/keygrip@*":
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72"
|
||||||
|
integrity sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==
|
||||||
|
|
||||||
|
"@types/koa-compose@*":
|
||||||
|
version "3.2.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/koa-compose/-/koa-compose-3.2.5.tgz#85eb2e80ac50be95f37ccf8c407c09bbe3468e9d"
|
||||||
|
integrity sha512-B8nG/OoE1ORZqCkBVsup/AKcvjdgoHnfi4pZMn5UwAPCbhk/96xyv284eBYW8JlQbQ7zDmnpFr68I/40mFoIBQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/koa" "*"
|
||||||
|
|
||||||
|
"@types/koa@*":
|
||||||
|
version "2.13.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.5.tgz#64b3ca4d54e08c0062e89ec666c9f45443b21a61"
|
||||||
|
integrity sha512-HSUOdzKz3by4fnqagwthW/1w/yJspTgppyyalPVbgZf8jQWvdIXcVW5h2DGtw4zYntOaeRGx49r1hxoPWrD4aA==
|
||||||
|
dependencies:
|
||||||
|
"@types/accepts" "*"
|
||||||
|
"@types/content-disposition" "*"
|
||||||
|
"@types/cookies" "*"
|
||||||
|
"@types/http-assert" "*"
|
||||||
|
"@types/http-errors" "*"
|
||||||
|
"@types/keygrip" "*"
|
||||||
|
"@types/koa-compose" "*"
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/koa@2.13.4":
|
||||||
|
version "2.13.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.4.tgz#10620b3f24a8027ef5cbae88b393d1b31205726b"
|
||||||
|
integrity sha512-dfHYMfU+z/vKtQB7NUrthdAEiSvnLebvBjwHtfFmpZmB7em2N3WVQdHgnFq+xvyVgxW5jKDmjWfLD3lw4g4uTw==
|
||||||
|
dependencies:
|
||||||
|
"@types/accepts" "*"
|
||||||
|
"@types/content-disposition" "*"
|
||||||
|
"@types/cookies" "*"
|
||||||
|
"@types/http-assert" "*"
|
||||||
|
"@types/http-errors" "*"
|
||||||
|
"@types/keygrip" "*"
|
||||||
|
"@types/koa-compose" "*"
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/mime@^1":
|
||||||
|
version "1.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
|
||||||
|
integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==
|
||||||
|
|
||||||
|
"@types/node@*":
|
||||||
|
version "18.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.6.tgz#0ba49ac517ad69abe7a1508bc9b3a5483df9d5d7"
|
||||||
|
integrity sha512-/xUq6H2aQm261exT6iZTMifUySEt4GR5KX8eYyY+C4MSNPqSh9oNIP7tz2GLKTlFaiBbgZNxffoR3CVRG+cljw==
|
||||||
|
|
||||||
"@types/node@14.18.20":
|
"@types/node@14.18.20":
|
||||||
version "14.18.20"
|
version "14.18.20"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.20.tgz#268f028b36eaf51181c3300252f605488c4f0650"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.20.tgz#268f028b36eaf51181c3300252f605488c4f0650"
|
||||||
integrity sha512-Q8KKwm9YqEmUBRsqJ2GWJDtXltBDxTdC4m5vTdXBolu2PeQh8LX+f6BTwU+OuXPu37fLxoN6gidqBmnky36FXA==
|
integrity sha512-Q8KKwm9YqEmUBRsqJ2GWJDtXltBDxTdC4m5vTdXBolu2PeQh8LX+f6BTwU+OuXPu37fLxoN6gidqBmnky36FXA==
|
||||||
|
|
||||||
|
"@types/qs@*":
|
||||||
|
version "6.9.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
|
||||||
|
integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==
|
||||||
|
|
||||||
|
"@types/range-parser@*":
|
||||||
|
version "1.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
|
||||||
|
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
|
||||||
|
|
||||||
|
"@types/serve-static@*":
|
||||||
|
version "1.13.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.10.tgz#f5e0ce8797d2d7cc5ebeda48a52c96c4fa47a8d9"
|
||||||
|
integrity sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/mime" "^1"
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
balanced-match@^1.0.0:
|
balanced-match@^1.0.0:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||||
|
|
|
@ -17,7 +17,9 @@ const arg = process.argv.slice(2)[0]
|
||||||
*/
|
*/
|
||||||
updateDotEnv({
|
updateDotEnv({
|
||||||
ACCOUNT_PORTAL_URL:
|
ACCOUNT_PORTAL_URL:
|
||||||
arg === "enable" ? "http://local.com:10001" : "http://localhost:10001",
|
arg === "enable"
|
||||||
|
? "http://account.local.com:10001"
|
||||||
|
: "http://localhost:10001",
|
||||||
COOKIE_DOMAIN: arg === "enable" ? ".local.com" : "",
|
COOKIE_DOMAIN: arg === "enable" ? ".local.com" : "",
|
||||||
PLATFORM_URL:
|
PLATFORM_URL:
|
||||||
arg === "enable" ? "http://local.com:10000" : "http://localhost:10000",
|
arg === "enable" ? "http://local.com:10000" : "http://localhost:10000",
|
||||||
|
|
|
@ -144,6 +144,7 @@ exports.updateSelf = async ctx => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove the old password from the user before sending events
|
// remove the old password from the user before sending events
|
||||||
|
user._rev = response.rev
|
||||||
delete user.password
|
delete user.password
|
||||||
await events.user.updated(user)
|
await events.user.updated(user)
|
||||||
if (passwordChange) {
|
if (passwordChange) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
jest.mock("nodemailer")
|
jest.mock("nodemailer")
|
||||||
import { TestConfiguration, API } from "../../../../tests"
|
import { TestConfiguration, API, mocks } from "../../../../tests"
|
||||||
import { events } from "@budibase/backend-core"
|
import { events } from "@budibase/backend-core"
|
||||||
|
|
||||||
describe("/api/global/self", () => {
|
describe("/api/global/self", () => {
|
||||||
|
@ -26,6 +26,9 @@ describe("/api/global/self", () => {
|
||||||
delete user.password
|
delete user.password
|
||||||
const res = await api.self.updateSelf(user)
|
const res = await api.self.updateSelf(user)
|
||||||
|
|
||||||
|
const dbUser = await config.getUser(user.email)
|
||||||
|
user._rev = dbUser._rev
|
||||||
|
user.dayPassRecordedAt = mocks.date.MOCK_DATE.toISOString()
|
||||||
expect(res.body._id).toBe(user._id)
|
expect(res.body._id).toBe(user._id)
|
||||||
expect(events.user.updated).toBeCalledTimes(1)
|
expect(events.user.updated).toBeCalledTimes(1)
|
||||||
expect(events.user.updated).toBeCalledWith(user)
|
expect(events.user.updated).toBeCalledWith(user)
|
||||||
|
@ -39,6 +42,9 @@ describe("/api/global/self", () => {
|
||||||
user.password = "newPassword"
|
user.password = "newPassword"
|
||||||
const res = await api.self.updateSelf(user)
|
const res = await api.self.updateSelf(user)
|
||||||
|
|
||||||
|
const dbUser = await config.getUser(user.email)
|
||||||
|
user._rev = dbUser._rev
|
||||||
|
user.dayPassRecordedAt = mocks.date.MOCK_DATE.toISOString()
|
||||||
delete user.password
|
delete user.password
|
||||||
expect(res.body._id).toBe(user._id)
|
expect(res.body._id).toBe(user._id)
|
||||||
expect(events.user.updated).toBeCalledTimes(1)
|
expect(events.user.updated).toBeCalledTimes(1)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import env from "../../environment"
|
import env from "../../environment"
|
||||||
import { events, accounts, tenancy } from "@budibase/backend-core"
|
import { events, accounts, tenancy } from "@budibase/backend-core"
|
||||||
import { User, UserRoles, CloudAccount } from "@budibase/types"
|
import { User, UserRoles, CloudAccount } from "@budibase/types"
|
||||||
|
import { users as pro } from "@budibase/pro"
|
||||||
|
|
||||||
export const handleDeleteEvents = async (user: any) => {
|
export const handleDeleteEvents = async (user: any) => {
|
||||||
await events.user.deleted(user)
|
await events.user.deleted(user)
|
||||||
|
@ -51,7 +52,10 @@ const handleAppRoleEvents = async (user: any, existingUser: any) => {
|
||||||
await unassignAppRoleEvents(user, roles, existingRoles)
|
await unassignAppRoleEvents(user, roles, existingRoles)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const handleSaveEvents = async (user: any, existingUser: any) => {
|
export const handleSaveEvents = async (
|
||||||
|
user: User,
|
||||||
|
existingUser: User | undefined
|
||||||
|
) => {
|
||||||
const tenantId = tenancy.getTenantId()
|
const tenantId = tenancy.getTenantId()
|
||||||
let tenantAccount: CloudAccount | undefined
|
let tenantAccount: CloudAccount | undefined
|
||||||
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
||||||
|
|
|
@ -1,37 +1,35 @@
|
||||||
import env from "../../environment"
|
import env from "../../environment"
|
||||||
import { quotas } from "@budibase/pro"
|
|
||||||
import * as apps from "../../utilities/appService"
|
import * as apps from "../../utilities/appService"
|
||||||
import * as eventHelpers from "./events"
|
import * as eventHelpers from "./events"
|
||||||
import {
|
import {
|
||||||
tenancy,
|
|
||||||
utils,
|
|
||||||
db as dbUtils,
|
|
||||||
constants,
|
|
||||||
cache,
|
|
||||||
users as usersCore,
|
|
||||||
deprovisioning,
|
|
||||||
sessions,
|
|
||||||
HTTPError,
|
|
||||||
accounts,
|
accounts,
|
||||||
migrations,
|
cache,
|
||||||
StaticDatabases,
|
constants,
|
||||||
ViewName,
|
db as dbUtils,
|
||||||
|
deprovisioning,
|
||||||
events,
|
events,
|
||||||
|
HTTPError,
|
||||||
|
migrations,
|
||||||
|
sessions,
|
||||||
|
tenancy,
|
||||||
|
users as usersCore,
|
||||||
|
utils,
|
||||||
|
ViewName,
|
||||||
} from "@budibase/backend-core"
|
} from "@budibase/backend-core"
|
||||||
import {
|
import {
|
||||||
MigrationType,
|
|
||||||
PlatformUserByEmail,
|
|
||||||
User,
|
|
||||||
BulkCreateUsersResponse,
|
|
||||||
CreateUserResponse,
|
|
||||||
BulkDeleteUsersResponse,
|
|
||||||
CloudAccount,
|
|
||||||
AllDocsResponse,
|
|
||||||
RowResponse,
|
|
||||||
BulkDocsResponse,
|
|
||||||
AccountMetadata,
|
AccountMetadata,
|
||||||
|
AllDocsResponse,
|
||||||
|
BulkCreateUsersResponse,
|
||||||
|
BulkDeleteUsersResponse,
|
||||||
|
BulkDocsResponse,
|
||||||
|
CloudAccount,
|
||||||
|
CreateUserResponse,
|
||||||
InviteUsersRequest,
|
InviteUsersRequest,
|
||||||
InviteUsersResponse,
|
InviteUsersResponse,
|
||||||
|
MigrationType,
|
||||||
|
PlatformUserByEmail,
|
||||||
|
RowResponse,
|
||||||
|
User,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { groups as groupUtils } from "@budibase/pro"
|
import { groups as groupUtils } from "@budibase/pro"
|
||||||
import { sendEmail } from "../../utilities/email"
|
import { sendEmail } from "../../utilities/email"
|
||||||
|
@ -120,7 +118,7 @@ interface SaveUserOpts {
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildUser = async (
|
const buildUser = async (
|
||||||
user: any,
|
user: User,
|
||||||
opts: SaveUserOpts = {
|
opts: SaveUserOpts = {
|
||||||
hashPassword: true,
|
hashPassword: true,
|
||||||
requirePassword: true,
|
requirePassword: true,
|
||||||
|
@ -230,16 +228,7 @@ export const save = async (
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// save the user to db
|
// save the user to db
|
||||||
let response
|
let response = await db.put(builtUser)
|
||||||
const putUserFn = () => {
|
|
||||||
return db.put(builtUser)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventHelpers.isAddingBuilder(builtUser, dbUser)) {
|
|
||||||
response = await quotas.addDeveloper(putUserFn)
|
|
||||||
} else {
|
|
||||||
response = await putUserFn()
|
|
||||||
}
|
|
||||||
builtUser._rev = response.rev
|
builtUser._rev = response.rev
|
||||||
|
|
||||||
await eventHelpers.handleSaveEvents(builtUser, dbUser)
|
await eventHelpers.handleSaveEvents(builtUser, dbUser)
|
||||||
|
@ -388,13 +377,8 @@ export const bulkCreate = async (
|
||||||
newUsers.push(newUser)
|
newUsers.push(newUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Figure out how many builders we are adding and create the promises
|
// create the promises array that will be called by bulkDocs
|
||||||
// array that will be called by bulkDocs
|
|
||||||
let builderCount = 0
|
|
||||||
newUsers.forEach((user: any) => {
|
newUsers.forEach((user: any) => {
|
||||||
if (eventHelpers.isAddingBuilder(user, null)) {
|
|
||||||
builderCount++
|
|
||||||
}
|
|
||||||
usersToSave.push(
|
usersToSave.push(
|
||||||
buildUser(
|
buildUser(
|
||||||
user,
|
user,
|
||||||
|
@ -408,14 +392,14 @@ export const bulkCreate = async (
|
||||||
})
|
})
|
||||||
|
|
||||||
const usersToBulkSave = await Promise.all(usersToSave)
|
const usersToBulkSave = await Promise.all(usersToSave)
|
||||||
await quotas.addDevelopers(() => db.bulkDocs(usersToBulkSave), builderCount)
|
await db.bulkDocs(usersToBulkSave)
|
||||||
|
|
||||||
// Post processing of bulk added users, i.e events and cache operations
|
// Post processing of bulk added users, i.e events and cache operations
|
||||||
for (const user of usersToBulkSave) {
|
for (const user of usersToBulkSave) {
|
||||||
// TODO: Refactor to bulk insert users into the info db
|
// TODO: Refactor to bulk insert users into the info db
|
||||||
// instead of relying on looping tenant creation
|
// instead of relying on looping tenant creation
|
||||||
await addTenant(tenantId, user._id, user.email)
|
await addTenant(tenantId, user._id, user.email)
|
||||||
await eventHelpers.handleSaveEvents(user, null)
|
await eventHelpers.handleSaveEvents(user, undefined)
|
||||||
await apps.syncUserInApps(user._id)
|
await apps.syncUserInApps(user._id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -475,8 +459,6 @@ export const bulkDelete = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
let groupsToModify: any = {}
|
let groupsToModify: any = {}
|
||||||
let builderCount = 0
|
|
||||||
|
|
||||||
// Get users and delete
|
// Get users and delete
|
||||||
const allDocsResponse: AllDocsResponse<User> = await db.allDocs({
|
const allDocsResponse: AllDocsResponse<User> = await db.allDocs({
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
|
@ -497,11 +479,6 @@ export const bulkDelete = async (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also figure out how many builders are being deleted
|
|
||||||
if (eventHelpers.isAddingBuilder(user.doc, null)) {
|
|
||||||
builderCount++
|
|
||||||
}
|
|
||||||
|
|
||||||
return user.doc
|
return user.doc
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -519,7 +496,6 @@ export const bulkDelete = async (
|
||||||
for (let user of usersToDelete) {
|
for (let user of usersToDelete) {
|
||||||
await bulkDeleteProcessing(user)
|
await bulkDeleteProcessing(user)
|
||||||
}
|
}
|
||||||
await quotas.removeDevelopers(builderCount)
|
|
||||||
|
|
||||||
// Build Response
|
// Build Response
|
||||||
// index users by id
|
// index users by id
|
||||||
|
@ -574,7 +550,6 @@ export const destroy = async (id: string, currentUser: any) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
await eventHelpers.handleDeleteEvents(dbUser)
|
await eventHelpers.handleDeleteEvents(dbUser)
|
||||||
await quotas.removeUser(dbUser)
|
|
||||||
await cache.user.invalidateUser(userId)
|
await cache.user.invalidateUser(userId)
|
||||||
await sessions.invalidateSessions(userId, { reason: "deletion" })
|
await sessions.invalidateSessions(userId, { reason: "deletion" })
|
||||||
// let server know to sync user
|
// let server know to sync user
|
||||||
|
|
|
@ -2,10 +2,9 @@ import {
|
||||||
BulkCreateUsersRequest,
|
BulkCreateUsersRequest,
|
||||||
BulkCreateUsersResponse,
|
BulkCreateUsersResponse,
|
||||||
BulkDeleteUsersRequest,
|
BulkDeleteUsersRequest,
|
||||||
CreateUserResponse,
|
BulkDeleteUsersResponse,
|
||||||
InviteUsersRequest,
|
InviteUsersRequest,
|
||||||
User,
|
User,
|
||||||
UserDetails,
|
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import TestConfiguration from "../TestConfiguration"
|
import TestConfiguration from "../TestConfiguration"
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue