Merge commit

This commit is contained in:
Dean 2022-09-13 11:52:31 +01:00
parent 6ed4fede00
commit 98333b8791
19 changed files with 707 additions and 81 deletions

View File

@ -4,22 +4,30 @@
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={() => {
message.onChange()
}}
showCloseButton={typeof message.showCloseButton === "boolean"
? message.showCloseButton
: true}
> >
{$banner.message} <TooltipWrapper tooltip={"test"}>
{message.message}
</TooltipWrapper>
</Banner> </Banner>
</div> </div>
{/if} {/each}
</div> </div>
</Portal> </Portal>

View File

@ -1,7 +1,9 @@
import { writable } from "svelte/store" import { writable } from "svelte/store"
export function createBannerStore() { export function createBannerStore() {
const DEFAULT_CONFIG = {} const DEFAULT_CONFIG = {
messages: [],
}
const banner = writable(DEFAULT_CONFIG) const banner = writable(DEFAULT_CONFIG)
@ -28,9 +30,23 @@ export function createBannerStore() {
await show(config) await show(config)
} }
const queue = async entries => {
banner.update(store => {
const sorted = [...store.messages, ...entries].sort(
(a, b) => a.priority > b.priority
)
return {
...store,
messages: sorted,
}
})
}
return { return {
subscribe: banner.subscribe, subscribe: banner.subscribe,
showStatus, showStatus,
show,
queue,
} }
} }

View File

@ -54,7 +54,6 @@
transform: scale(0.75); transform: scale(0.75);
} }
.icon-small { .icon-small {
margin-top: -2px; margin-bottom: -2px;
margin-bottom: -5px;
} }
</style> </style>

View File

@ -34,6 +34,7 @@ export { default as Layout } from "./Layout/Layout.svelte"
export { default as Page } from "./Layout/Page.svelte" export { default as Page } from "./Layout/Page.svelte"
export { default as Link } from "./Link/Link.svelte" export { default as Link } from "./Link/Link.svelte"
export { default as Tooltip } from "./Tooltip/Tooltip.svelte" export { default as Tooltip } from "./Tooltip/Tooltip.svelte"
export { default as TooltipWrapper } from "./Tooltip/TooltipWrapper.svelte"
export { default as Menu } from "./Menu/Menu.svelte" export { default as Menu } from "./Menu/Menu.svelte"
export { default as MenuSection } from "./Menu/Section.svelte" export { default as MenuSection } from "./Menu/Section.svelte"
export { default as MenuSeparator } from "./Menu/Separator.svelte" export { default as MenuSeparator } from "./Menu/Separator.svelte"

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,51 @@
<script>
import { Modal, ModalContent, Body } from "@budibase/bbui"
import { auth, admin } from "stores/portal"
export let onDismiss = () => {}
export let onShow = () => {}
let accountDowngradeModal
const upgradeUrl = `${$admin.accountPortalUrl}/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 Business Subscription failed and we have downgraded
your account to the <span class="free-plan">Free plan</span>.
</Body>
<Body>
Update to Business to get all your apps and user sessions back up and
running.
</Body>
{#if !$auth.user.accountPortalAccess}
<Body>Please contact the account holder.</Body>
{/if}
</ModalContent>
</Modal>
<style>
.free-plan {
font-weight: 600;
}
</style>

View File

@ -0,0 +1,46 @@
<script>
import { Modal, ModalContent, Body } from "@budibase/bbui"
import { auth, admin } from "stores/portal"
export let onDismiss = () => {}
let appLimitModal
const upgradeUrl = `${$admin.accountPortalUrl}/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.
</Body>
{#if !$auth.user.accountPortalAccess}
<Body>Please contact the account holder.</Body>
{/if}
</ModalContent>
</Modal>
<style>
.free-plan {
font-weight: 600;
}
</style>

View File

@ -0,0 +1,54 @@
<script>
import { Modal, ModalContent, Body } from "@budibase/bbui"
import { licensing, auth, admin } from "stores/portal"
export let onDismiss = () => {}
export let onShow = () => {}
let sessionsModal
const outOfSessionsTitle = "You are almost out of sessions"
const upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade`
$: daysRemaining = $licensing.quotaResetDaysRemaining
$: sessionsUsed = $licensing.usageMetrics?.dayPasses
export function show() {
sessionsModal.show()
}
export function hide() {
sessionsModal.hide()
}
</script>
<Modal bind:this={sessionsModal} on:show={onShow} on:hide={onDismiss}>
{#if $auth.user.accountPortalAccess}
<ModalContent
title={outOfSessionsTitle}
size="M"
confirmText="Upgrade"
onConfirm={() => {
window.location.href = upgradeUrl
}}
>
<Body>
You have used <span class="session_percent">{sessionsUsed}%</span> of
your plans Day Passes with {daysRemaining} day{daysRemaining == 1
? ""
: "s"} remaining.
</Body>
<Body>Upgrade your account to prevent your apps from going offline.</Body>
</ModalContent>
{:else}
<ModalContent title={outOfSessionsTitle} size="M" showCancelButton={false}>
<Body>
You have used <span class="session_percent">{sessionsUsed}%</span> of
your plans Day Passes with {daysRemaining} day{daysRemaining == 1
? ""
: "s"} remaining.
</Body>
<Body>Please contact your account holder.</Body>
</ModalContent>
{/if}
</Modal>

View File

@ -0,0 +1,117 @@
<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 "./banners"
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
},
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()
})
}
$: if (userLoaded && licensingLoaded && loaded) {
queuedModals = processModals()
queuedBanners = getBanners()
showNext()
banner.queue(queuedBanners)
}
const showNext = () => {
if (currentModalCfg) {
currentModalCfg.cache()
}
if (queuedModals.length) {
currentModalCfg = queuedModals.shift()
currentModalCfg.action()
} else {
currentModalCfg = null
}
}
onMount(async () => {
auth.subscribe(state => {
if (state.user && !userLoaded) {
userLoaded = true
}
})
licensing.subscribe(state => {
if (state.usageMetrics && !licensingLoaded) {
licensingLoaded = true
}
})
temporalStore.subscribe(state => {
console.log("Stored temporal ", state)
})
loaded = true
})
</script>
<DayPassWarningModal bind:this={dayPassModal} onDismiss={showNext} />
<PaymentFailedModal bind:this={paymentFailedModal} onDismiss={showNext} />
<AccountDowngradedModal
bind:this={accountDowngradeModal}
onDismiss={showNext}
/>

View File

@ -0,0 +1,87 @@
<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 pastDueAt
const paymentFailedTitle = "Payment failed"
const upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade`
export function show() {
paymentFailedModal.show()
}
export function hide() {
paymentFailedModal.hide()
}
onMount(() => {
auth.subscribe(state => {
if (state.user && state.user.license?.billing?.subscription) {
pastDueAt = new Date(
state.user.license?.billing?.subscription.pastDueAt * 1000
)
}
})
})
</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 business plan 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.paymentDueDaysRemaining} day${
$licensing.paymentDueDaysRemaining == 1 ? "" : "s"
} remaining`}
<span class="tooltip">
<TooltipWrapper tooltip={pastDueAt.toString()} size="S" />
</span>
</div>
</Body>
</ModalContent>
{:else}
<ModalContent title={paymentFailedTitle} size="M" showCancelButton={false}>
<Body>The payment for your business plan 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.paymentDueDaysRemaining} day${
$licensing.paymentDueDaysRemaining == 1 ? "" : "s"
} remaining`}
<span class="tooltip">
<TooltipWrapper tooltip={pastDueAt.toString()} size="S" />
</span>
</div>
</Body>
</ModalContent>
{/if}
</Modal>
<style>
.tooltip-root {
display: flex;
align-items: center;
}
</style>

View File

@ -0,0 +1,132 @@
import { ExpiringKeys } from "./constants"
import { temporalStore } from "builderStore"
import { admin, auth, licensing } from "stores/portal"
import { get } from "svelte/store"
const oneDayInSeconds = 86400
const upgradeUrl = `${get(admin).accountPortalUrl}/portal/upgrade`
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 = upgradeUrl
},
}
}
const buildUsageInfoBanner = (metricKey, metricLabel, cacheKey, percentage) => {
const appAuth = get(auth)
const appLicensing = get(licensing)
let bannerConfig = {
key: cacheKey,
type: "info",
onChange: () => {
defaultCacheFn(cacheKey)
},
message: `You have used ${
appLicensing?.usageMetrics[metricKey]
}% of your monthly usage of ${metricLabel} with ${
appLicensing.quotaResetDaysRemaining
} day${
appLicensing.quotaResetDaysRemaining == 1 ? "" : "s"
} remaining. All apps will be taken offline if this limit is reached. ${
appAuth.user.accountPortalAccess
? ""
: "Please contact your account holder."
}`,
criteria: () => {
return appLicensing?.usageMetrics[metricKey] >= percentage
},
priority: 0, //Banners.Priority 0, 1, 2 ??
}
return !get(auth).user.accountPortalAccess
? bannerConfig
: {
...bannerConfig,
...defaultAction(cacheKey),
}
}
const buildDayPassBanner = () => {
const appAuth = get(auth)
if (get(licensing)?.usageMetrics["dayPasses"] >= 100) {
return {
key: "max_dayPasses",
type: "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."
}`,
...defaultAction(),
showCloseButton: false,
}
}
return buildUsageInfoBanner(
"dayPasses",
"Day Passes",
ExpiringKeys.LICENSING_DAYPASS_WARNING_BANNER,
90
)
}
const buildPaymentFailedBanner = () => {
return {
key: "payment_Failed",
type: "negative",
criteria: () => {
return get(licensing)?.accountPastDue
},
message: `Payment Failed - Please update your billing details or your account will be downgrades in
${get(licensing)?.paymentDueDaysRemaining} day${
get(licensing)?.paymentDueDaysRemaining == 1 ? "" : "s"
}`,
...defaultAction(),
showCloseButton: false,
}
}
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 // could be an array [50,75,90]
),
].filter(licensingBanner => {
return (
!temporalStore.actions.getExpiring(licensingBanner.key) &&
licensingBanner.criteria()
)
})
}

View File

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

View File

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

View File

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

View File

@ -16,6 +16,7 @@
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 ExportAppModal from "components/start/ExportAppModal.svelte" import ExportAppModal from "components/start/ExportAppModal.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"
@ -34,6 +35,7 @@
let creationModal let creationModal
let updatingModal let updatingModal
let exportModal let exportModal
let appLimitModal
let creatingApp = false let creatingApp = false
let loaded = $apps?.length || $templates?.length let loaded = $apps?.length || $templates?.length
let searchTerm = "" let searchTerm = ""
@ -126,8 +128,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
@ -227,6 +231,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."
@ -243,10 +251,6 @@
notifications.error("Error loading apps and templates") notifications.error("Error loading apps and templates")
} }
loaded = true loaded = true
//Testing
await licensing.getQuotaUsage()
await licensing.getTestData()
}) })
</script> </script>
@ -415,6 +419,8 @@
<ExportAppModal app={selectedApp} /> <ExportAppModal app={selectedApp} />
</Modal> </Modal>
<AppLimitModal bind:this={appLimitModal} />
<style> <style>
.appTable { .appTable {
border-top: var(--border-light); border-top: var(--border-light);

View File

@ -1,12 +1,16 @@
import { writable, get } from "svelte/store" import { writable, get } from "svelte/store"
import { API } from "api" import { API } from "api"
import { auth } from "stores/portal" 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 = {
@ -19,65 +23,86 @@ export const createLicensingStore = () => {
} }
}) })
}, },
getTestData: async () => { getUsageMetrics: async () => {
const tenantId = get(auth).tenantId
console.log("Tenant ", tenantId)
const license = get(auth).user.license
console.log("User LICENSE ", license)
// Pull out the usage.
const quota = get(store).quotaUsage const quota = get(store).quotaUsage
console.log("Quota usage", quota) const license = get(auth).user.license
const now = Date.now()
const nowSeconds = now / 1000
// Would only initialise the usage elements if the account element is present. const getMetrics = (keys, license, quota) => {
console.log("User account ", get(auth).user.account) if (!license || !quota || !keys) {
return {}
}
return keys.reduce((acc, key) => {
const quotaLimit = license[key].value
const quotaUsed = (quota[key] / quotaLimit) * 100
//separate into functions that pass in both the usage and quota // Catch for sessions
//easier to test key = key === "sessions" ? "dayPasses" : key
const getMonthlyMetrics = (license, quota) => { acc[key] = quotaLimit > -1 ? Math.round(quotaUsed) : -1
return ["sessions", "queries", "automations"].reduce((acc, key) => {
const quotaLimit = license.quotas.usage.monthly[key].value
acc[key] =
quotaLimit > -1
? (quota.monthly.current[key] / quotaLimit) * 100
: -1
return acc return acc
}, {}) }, {})
} }
const monthlyMetrics = getMetrics(
["sessions", "queries", "automations"],
license.quotas.usage.monthly,
quota.monthly.current
)
const staticMetrics = getMetrics(
["apps", "rows"],
license.quotas.usage.static,
quota.usageQuota
)
const getStaticMetrics = (license, quota) => { // DEBUG
return ["apps", "rows"].reduce((acc, key) => { console.log("Store licensing val ", {
const quotaLimit = license.quotas.usage.monthly[key].value ...monthlyMetrics,
acc[key] = ...staticMetrics,
quotaLimit > -1 ? (quota.usageQuota[key] / quotaLimit) * 100 : -1
return acc
}, {})
}
const modQuotaStr = JSON.stringify(quota)
const cloneQuota = JSON.parse(modQuotaStr)
cloneQuota.monthly.current.sessions = 4
const monthlyMetrics = getMonthlyMetrics(license, cloneQuota)
const staticMetrics = getStaticMetrics(license, cloneQuota)
console.log("Monthly Usage Metrics ", monthlyMetrics)
console.log("Static Usage Metrics ", staticMetrics)
const flagged = Object.keys(monthlyMetrics).filter(key => {
return monthlyMetrics[key] >= 100
}) })
console.log(flagged) let subscriptionDaysRemaining
if (license?.billing?.subscription) {
const currentPeriodEnd = license.billing.subscription.currentPeriodEnd
const currentPeriodEndMilliseconds = currentPeriodEnd * 1000
// store.update(state => { subscriptionDaysRemaining = Math.round(
// return { (currentPeriodEndMilliseconds - now) / oneDayInMilliseconds
// ...state, )
// metrics, }
// }
// }) const quotaResetDaysRemaining =
quota.quotaReset > now
? Math.round((quota.quotaReset - now) / oneDayInMilliseconds)
: 0
const accountDowngraded =
license?.billing?.subscription?.status === StripeStatus.PAST_DUE &&
license?.plan === Constants.PlanType.FREE
const accountPastDue =
nowSeconds >= license?.billing?.subscription?.currentPeriodEnd &&
nowSeconds <= license?.billing?.subscription?.pastDueAt &&
license?.billing?.subscription?.status === StripeStatus.PAST_DUE &&
!accountDowngraded
const pastDueAtSeconds = license?.billing?.subscription?.pastDueAt
const pastDueAtMilliseconds = pastDueAtSeconds * 1000
const paymentDueDaysRemaining = Math.round(
(pastDueAtMilliseconds - now) / oneDayInMilliseconds
)
store.update(state => {
return {
...state,
usageMetrics: { ...monthlyMetrics, ...staticMetrics },
subscriptionDaysRemaining,
paymentDueDaysRemaining,
quotaResetDaysRemaining,
accountDowngraded,
accountPastDue,
}
})
}, },
} }