Apps Page update to hide apps when sessions are maxed. General refactoring and updates to the licensing notification flows.

This commit is contained in:
Dean 2022-09-14 18:04:58 +01:00
parent e29df0b7b5
commit bcd6b711bf
14 changed files with 198 additions and 139 deletions

View File

@ -16,13 +16,15 @@
extraButtonText={message.extraButtonText}
extraButtonAction={message.extraButtonAction}
on:change={() => {
message.onChange()
if (message.onChange) {
message.onChange()
}
}}
showCloseButton={typeof message.showCloseButton === "boolean"
? message.showCloseButton
: true}
>
<TooltipWrapper tooltip={"test"}>
<TooltipWrapper tooltip={message.tooltip} disabled={false}>
{message.message}
</TooltipWrapper>
</Banner>

View File

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

View File

@ -4,6 +4,7 @@
export let tooltip = ""
export let size = "M"
export let disabled = true
let showTooltip = false
</script>
@ -19,7 +20,7 @@
on:mouseleave={() => (showTooltip = false)}
on:focus
>
<Icon name="InfoOutline" size="S" disabled={true} />
<Icon name="InfoOutline" size="S" {disabled} />
</div>
{#if showTooltip}
<div class="tooltip">

View File

@ -95,7 +95,7 @@ export { default as clickOutside } from "./Actions/click_outside"
// Stores
export { notifications, createNotificationStore } from "./Stores/notifications"
export { banner } from "./Stores/banner"
export { banner, BANNER_TYPES } from "./Stores/banner"
// Helpers
export * as Helpers from "./helpers"

View File

@ -4,8 +4,7 @@ import { get } from "svelte/store"
export const getTemporalStore = () => {
const initialValue = {}
//const appId = window["##BUDIBASE_APP_ID##"] || "app"
const localStorageKey = `${123}.bb-temporal`
const localStorageKey = `bb-temporal`
const store = createLocalStorageStore(localStorageKey, initialValue)
const setExpiring = (key, data, duration) => {

View File

@ -7,7 +7,8 @@
let accountDowngradeModal
const upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade`
$: accountUrl = $admin.accountPortalUrl
$: upgradeUrl = `${accountUrl}/portal/upgrade`
export function show() {
accountDowngradeModal.show()
@ -31,15 +32,12 @@
: 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.
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.</Body>
<Body>Please contact the account holder to upgrade.</Body>
{/if}
</ModalContent>
</Modal>

View File

@ -6,7 +6,8 @@
let appLimitModal
const upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade`
$: accountUrl = $admin.accountPortalUrl
$: upgradeUrl = `${accountUrl}/portal/upgrade`
export function show() {
appLimitModal.show()
@ -19,7 +20,7 @@
<Modal bind:this={appLimitModal} on:hide={onDismiss}>
<ModalContent
title="Upgrade to get more apps"
title="Upgrade to get more apps "
size="M"
showCancelButton={false}
confirmText={$auth.user.accountPortalAccess ? "Upgrade" : "Confirm"}
@ -31,10 +32,10 @@
>
<Body>
You are currently on our <span class="free-plan">Free plan</span>. Upgrade
to our Pro plan to get unlimited apps.
to our Pro plan to get unlimited apps and additional features.
</Body>
{#if !$auth.user.accountPortalAccess}
<Body>Please contact the account holder.</Body>
<Body>Please contact the account holder to upgrade.</Body>
{/if}
</ModalContent>
</Modal>

View File

@ -1,31 +1,40 @@
<script>
import { Modal, ModalContent, Body } from "@budibase/bbui"
import { Modal, ModalContent, Body, TooltipWrapper } from "@budibase/bbui"
import { licensing, auth, admin } from "stores/portal"
export let onDismiss = () => {}
export let onShow = () => {}
let sessionsModal
let dayPassModal
const outOfSessionsTitle = "You are almost out of sessions"
const upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade`
$: accountUrl = $admin.accountPortalUrl
$: upgradeUrl = `${accountUrl}/portal/upgrade`
$: daysRemaining = $licensing.quotaResetDaysRemaining
$: sessionsUsed = $licensing.usageMetrics?.dayPasses
$: 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() {
sessionsModal.show()
dayPassModal.show()
}
export function hide() {
sessionsModal.hide()
dayPassModal.hide()
}
</script>
<Modal bind:this={sessionsModal} on:show={onShow} on:hide={onDismiss}>
<Modal bind:this={dayPassModal} on:show={onShow} on:hide={onDismiss}>
{#if $auth.user.accountPortalAccess}
<ModalContent
title={outOfSessionsTitle}
title={dayPassesTitle}
size="M"
confirmText="Upgrade"
onConfirm={() => {
@ -33,22 +42,37 @@
}}
>
<Body>
You have used <span class="session_percent">{sessionsUsed}%</span> of
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>Upgrade your account to prevent your apps from going offline.</Body>
<Body>{dayPassesBody}</Body>
</ModalContent>
{:else}
<ModalContent title={outOfSessionsTitle} size="M" showCancelButton={false}>
<ModalContent title={dayPassesTitle} size="M" showCancelButton={false}>
<Body>
You have used <span class="session_percent">{sessionsUsed}%</span> of
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.</Body>
<Body>Please contact your account holder to upgrade.</Body>
</ModalContent>
{/if}
</Modal>
<style>
.tooltip {
display: inline-block;
}
.tooltip :global(.icon-container) {
margin: 0px;
}
</style>

View File

@ -6,7 +6,7 @@
import PaymentFailedModal from "./PaymentFailedModal.svelte"
import AccountDowngradedModal from "./AccountDowngradedModal.svelte"
import { ExpiringKeys } from "./constants"
import { getBanners } from "./banners"
import { getBanners } from "./licensingBanners"
import { banner } from "@budibase/bbui"
const oneDayInSeconds = 86400
@ -42,7 +42,7 @@
{
key: ExpiringKeys.LICENSING_PAYMENT_FAILED,
criteria: () => {
return $licensing.accountPastDue
return $licensing.accountPastDue && !$licensing.isFreePlan()
},
action: () => {
paymentFailedModal.show()
@ -69,14 +69,7 @@
})
}
$: if (userLoaded && licensingLoaded && loaded) {
queuedModals = processModals()
queuedBanners = getBanners()
showNext()
banner.queue(queuedBanners)
}
const showNext = () => {
const showNextModal = () => {
if (currentModalCfg) {
currentModalCfg.cache()
}
@ -88,6 +81,13 @@
}
}
$: if (userLoaded && licensingLoaded && loaded) {
queuedModals = processModals()
queuedBanners = getBanners()
showNextModal()
banner.queue(queuedBanners)
}
onMount(async () => {
auth.subscribe(state => {
if (state.user && !userLoaded) {
@ -100,18 +100,13 @@
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} />
<DayPassWarningModal bind:this={dayPassModal} onDismiss={showNextModal} />
<PaymentFailedModal bind:this={paymentFailedModal} onDismiss={showNextModal} />
<AccountDowngradedModal
bind:this={accountDowngradeModal}
onDismiss={showNext}
onDismiss={showNextModal}
/>

View File

@ -7,10 +7,11 @@
export let onShow = () => {}
let paymentFailedModal
let pastDueAt
let pastDueEndDate
const paymentFailedTitle = "Payment failed"
const upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade`
$: accountUrl = $admin.accountPortalUrl
$: upgradeUrl = `${accountUrl}/portal/upgrade`
export function show() {
paymentFailedModal.show()
@ -21,12 +22,8 @@
}
onMount(() => {
auth.subscribe(state => {
if (state.user && state.user.license?.billing?.subscription) {
pastDueAt = new Date(
state.user.license?.billing?.subscription.pastDueAt * 1000
)
}
licensing.subscribe(state => {
pastDueEndDate = state.pastDueEndDate
})
})
</script>
@ -48,11 +45,11 @@
</Body>
<Body weight={800}>
<div class="tooltip-root">
{`${$licensing.paymentDueDaysRemaining} day${
$licensing.paymentDueDaysRemaining == 1 ? "" : "s"
{`${$licensing.pastDueDaysRemaining} day${
$licensing.pastDueDaysRemaining == 1 ? "" : "s"
} remaining`}
<span class="tooltip">
<TooltipWrapper tooltip={pastDueAt.toString()} size="S" />
<TooltipWrapper tooltip={pastDueEndDate} size="S" />
</span>
</div>
</Body>
@ -67,11 +64,11 @@
<Body>Please contact your account holder.</Body>
<Body weight={800}>
<div class="tooltip-root">
{`${$licensing.paymentDueDaysRemaining} day${
$licensing.paymentDueDaysRemaining == 1 ? "" : "s"
{`${$licensing.pastDueDaysRemaining} day${
$licensing.pastDueDaysRemaining == 1 ? "" : "s"
} remaining`}
<span class="tooltip">
<TooltipWrapper tooltip={pastDueAt.toString()} size="S" />
<TooltipWrapper tooltip={pastDueEndDate} size="S" />
</span>
</div>
</Body>

View File

@ -6,7 +6,7 @@ export const ExpiringKeys = {
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",
LICENSING_QUERIES_WARNING_BANNER: "licensing_queries_warning_banner",
}
export const StripeStatus = {

View File

@ -2,9 +2,9 @@ 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 upgradeUrl = `${get(admin).accountPortalUrl}/portal/upgrade`
const defaultCacheFn = key => {
temporalStore.actions.setExpiring(key, {}, oneDayInSeconds)
@ -18,36 +18,47 @@ const defaultAction = key => {
extraButtonText: "Upgrade Plan",
extraButtonAction: () => {
defaultCacheFn(key)
window.location.href = upgradeUrl
window.location.href = `${get(admin).accountPortalUrl}/portal/upgrade`
},
}
}
const buildUsageInfoBanner = (metricKey, metricLabel, cacheKey, percentage) => {
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: "info",
type: BANNER_TYPES.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."
}`,
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] >= percentage
return appLicensing?.usageMetrics[metricKey] >= percentageThreshold
},
priority: 0, //Banners.Priority 0, 1, 2 ??
tooltip: appLicensing?.quotaResetDate,
}
return !get(auth).user.accountPortalAccess
@ -60,17 +71,18 @@ const buildUsageInfoBanner = (metricKey, metricLabel, cacheKey, percentage) => {
const buildDayPassBanner = () => {
const appAuth = get(auth)
const appLicensing = get(licensing)
if (get(licensing)?.usageMetrics["dayPasses"] >= 100) {
return {
key: "max_dayPasses",
type: "negative",
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."
: "Please contact your account holder to upgrade."
}`,
...defaultAction(),
showCloseButton: false,
@ -81,23 +93,35 @@ const buildDayPassBanner = () => {
"dayPasses",
"Day Passes",
ExpiringKeys.LICENSING_DAYPASS_WARNING_BANNER,
90
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: "negative",
type: BANNER_TYPES.NEGATIVE,
criteria: () => {
return get(licensing)?.accountPastDue
return get(licensing)?.accountPastDue && !get(licensing).isFreePlan()
},
message: `Payment Failed - Please update your billing details or your account will be downgrades in
${get(licensing)?.paymentDueDaysRemaining} day${
get(licensing)?.paymentDueDaysRemaining == 1 ? "" : "s"
${get(licensing)?.pastDueDaysRemaining} day${
get(licensing)?.pastDueDaysRemaining == 1 ? "" : "s"
}`,
...defaultAction(),
showCloseButton: false,
tooltip: get(licensing).pastDueEndDate,
}
}
@ -121,7 +145,7 @@ export const getBanners = () => {
"queries",
"Queries",
ExpiringKeys.LICENSING_QUERIES_WARNING_BANNER,
90 // could be an array [50,75,90]
90
),
].filter(licensingBanner => {
return (

View File

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

View File

@ -8,7 +8,6 @@ export const createLicensingStore = () => {
const DEFAULT = {
plans: {},
}
const oneDayInMilliseconds = 86400000
const store = writable(DEFAULT)
@ -26,8 +25,7 @@ export const createLicensingStore = () => {
getUsageMetrics: async () => {
const quota = get(store).quotaUsage
const license = get(auth).user.license
const now = Date.now()
const nowSeconds = now / 1000
const now = new Date()
const getMetrics = (keys, license, quota) => {
if (!license || !quota || !keys) {
@ -36,16 +34,12 @@ export const createLicensingStore = () => {
return keys.reduce((acc, key) => {
const quotaLimit = license[key].value
const quotaUsed = (quota[key] / quotaLimit) * 100
// Catch for sessions
key = key === "sessions" ? "dayPasses" : key
acc[key] = quotaLimit > -1 ? Math.round(quotaUsed) : -1
return acc
}, {})
}
const monthlyMetrics = getMetrics(
["sessions", "queries", "automations"],
["dayPasses", "queries", "automations"],
license.quotas.usage.monthly,
quota.monthly.current
)
@ -55,52 +49,50 @@ export const createLicensingStore = () => {
quota.usageQuota
)
// DEBUG
console.log("Store licensing val ", {
...monthlyMetrics,
...staticMetrics,
})
let subscriptionDaysRemaining
if (license?.billing?.subscription) {
const currentPeriodEnd = license.billing.subscription.currentPeriodEnd
const currentPeriodEndMilliseconds = currentPeriodEnd * 1000
subscriptionDaysRemaining = Math.round(
(currentPeriodEndMilliseconds - now) / oneDayInMilliseconds
)
const getDaysBetween = (dateStart, dateEnd) => {
return dateEnd > dateStart
? Math.round(
(dateEnd.getTime() - dateStart.getTime()) / oneDayInMilliseconds
)
: 0
}
const quotaResetDaysRemaining =
quota.quotaReset > now
? Math.round((quota.quotaReset - now) / 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 === Constants.PlanType.FREE
license?.plan.type === Constants.PlanType.FREE
const accountPastDue =
nowSeconds >= license?.billing?.subscription?.currentPeriodEnd &&
nowSeconds <= license?.billing?.subscription?.pastDueAt &&
license?.billing?.subscription?.status === StripeStatus.PAST_DUE &&
!accountDowngraded
const pastDueAtMilliseconds = license?.billing?.subscription?.pastDueAt
const downgradeAtMilliseconds =
license?.billing?.subscription?.downgradeAt
let pastDueDaysRemaining
let pastDueEndDate
const pastDueAtSeconds = license?.billing?.subscription?.pastDueAt
const pastDueAtMilliseconds = pastDueAtSeconds * 1000
const paymentDueDaysRemaining = Math.round(
(pastDueAtMilliseconds - now) / oneDayInMilliseconds
)
if (pastDueAtMilliseconds && downgradeAtMilliseconds) {
pastDueEndDate = new Date(downgradeAtMilliseconds)
pastDueDaysRemaining = getDaysBetween(
new Date(pastDueAtMilliseconds),
pastDueEndDate
)
}
store.update(state => {
return {
...state,
usageMetrics: { ...monthlyMetrics, ...staticMetrics },
subscriptionDaysRemaining,
paymentDueDaysRemaining,
quotaResetDaysRemaining,
quotaResetDate,
accountDowngraded,
accountPastDue,
accountPastDue: pastDueAtMilliseconds != null,
pastDueEndDate,
pastDueDaysRemaining,
isFreePlan: () => {
return license?.plan.type === Constants.PlanType.FREE
},
}
})
},