Usage page updates WIP

This commit is contained in:
Rory Powell 2022-08-30 09:53:16 +01:00
parent cb03d26980
commit d4d542e773
5 changed files with 381 additions and 67 deletions

View File

@ -1,10 +1,13 @@
<script>
import { Body, ProgressBar, Label } from "@budibase/bbui"
import { Body, ProgressBar, Heading, Icon, Link } from "@budibase/bbui"
import { admin } from "../../stores/portal"
import { onMount } from "svelte"
export let usage
export let warnWhenFull = false
let percentage
let unlimited = false
let showWarning = false
const isUnlimited = () => {
if (usage.total === -1) {
@ -14,29 +17,57 @@
}
const getPercentage = () => {
return Math.min(Math.ceil((usage.used / usage.total) * 100), 100)
return (usage.used / usage.total) * 100
}
const upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade`
onMount(() => {
unlimited = isUnlimited()
percentage = getPercentage()
if (warnWhenFull && percentage === 100) {
showWarning = true
}
})
</script>
<div class="usage">
<div class="info">
<Label size="XL">{usage.name}</Label>
<div class="header-container">
{#if showWarning}
<Icon name="Alert" />
{/if}
<div class="heading header-item">
<Heading size="XS" weight="light">{usage.name}</Heading>
</div>
</div>
{#if unlimited}
<Body size="S">{usage.used}</Body>
<Body size="S">{usage.used} / Unlimited</Body>
{:else}
<Body size="S">{usage.used} / {usage.total}</Body>
{/if}
</div>
<div>
{#if unlimited}
<Body size="S">Unlimited</Body>
<ProgressBar
showPercentage={false}
width={"100%"}
duration={1}
value={100}
/>
{:else}
<ProgressBar width={"100%"} duration={1} value={percentage} />
<ProgressBar
color={showWarning ? "red" : "green"}
showPercentage={false}
width={"100%"}
duration={1}
value={percentage}
/>
{/if}
{#if showWarning}
<Body size="S">
To get more queries <Link href={upgradeUrl}>upgrade your plan</Link>
</Body>
{/if}
</div>
</div>
@ -51,6 +82,13 @@
display: flex;
flex-direction: row;
justify-content: space-between;
gap: var(--spacing-m);
margin-bottom: 12px;
}
.header-container {
display: flex;
}
.heading {
margin-top: 3px;
margin-left: 5px;
}
</style>

View File

@ -0,0 +1,108 @@
<script>
import { Detail, Button, Heading, Layout, Body } from "@budibase/bbui"
export let description = ""
export let title = ""
export let subtitle = ""
export let primaryAction
export let secondaryAction
export let primaryActionText
export let secondaryActionText
export let primaryCta = false
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>
<div class="dash-card-title">
<Detail size="M">{subtitle}</Detail>
</div>
<div class="text-rows">
{#each textRows as row}
<Body>{row}</Body>
{/each}
</div>
</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>

View File

@ -0,0 +1,2 @@
export { default as Usage } from "./Usage.svelte"
export { default as DashCard } from "./UsageDashCard.svelte"

View File

@ -57,3 +57,10 @@ export const DefaultAppTheme = {
navBackground: "var(--spectrum-global-color-gray-50)",
navTextColor: "var(--spectrum-global-color-gray-800)",
}
export const PlanType = {
FREE: "free",
PRO: "pro",
BUSINESS: "business",
ENTERPRISE: "enterprise",
}

View File

@ -5,20 +5,39 @@
Heading,
Layout,
notifications,
Link,
Page,
Detail,
} from "@budibase/bbui"
import { onMount } from "svelte"
import { admin, auth, licensing } from "stores/portal"
import Usage from "components/usage/Usage.svelte"
import { admin, auth, licensing } from "../../../../stores/portal"
import { PlanType } from "../../../../constants"
import { DashCard, Usage } from "../../../../components/usage"
let staticUsage = []
let monthlyUsage = []
let price
let lastPayment
let cancelAt
let nextPayment
let balance
let loaded = false
let textRows = []
let daysRemainingInMonth
const upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade`
const manageUrl = `${$admin.accountPortalUrl}/portal/billing`
const warnUsage = ["Queries", "Automations", "Rows"]
$: quotaUsage = $licensing.quotaUsage
$: license = $auth.user?.license
const upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade`
const numberFormatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
})
const setMonthlyUsage = () => {
monthlyUsage = []
@ -34,6 +53,7 @@
}
}
}
monthlyUsage = monthlyUsage.sort((a, b) => a.name.localeCompare(b.name))
}
const setStaticUsage = () => {
@ -48,6 +68,52 @@
})
}
}
staticUsage = staticUsage.sort((a, b) => a.name.localeCompare(b.name))
}
const setNextPayment = () => {
const periodEnd = license?.billing.subscription?.currentPeriodEnd
const cancelAt = license?.billing.subscription?.cancelAt
if (periodEnd) {
if (cancelAt && periodEnd <= cancelAt) {
return
}
nextPayment = `Next payment: ${getLocaleDataString(periodEnd)}`
}
}
const setCancelAt = () => {
cancelAt = license?.billing.subscription?.cancelAt
}
const setLastPayment = () => {
const periodStart = license?.billing.subscription?.currentPeriodStart
if (periodStart) {
lastPayment = `Last payment: ${getLocaleDataString(periodStart)}`
}
}
const setBalance = () => {
const customerBalance = license?.billing.customer.balance
if (customerBalance) {
balance = `Balance: ${numberFormatter.format(
(customerBalance / 100) * -1
)}`
}
}
const getLocaleDataString = epoch => {
const date = new Date(epoch * 1000)
return date.toLocaleDateString("default", {
day: "numeric",
month: "long",
year: "numeric",
})
}
const setPrice = () => {
const planPrice = license.plan.price
price = `${numberFormatter.format(planPrice.amountMonthly / 100)} per month`
}
const capitalise = string => {
@ -56,6 +122,69 @@
}
}
const planTitle = () => {
return capitalise(license?.plan.type)
}
const planSubtitle = () => {
return `${license?.plan.price.sessions} day passes`
}
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("Subscription has been cancelled")
textRows.push(`${getDaysRemaining(cancelAt * 1000)} days remaining`)
} else {
if (price) {
textRows.push(price)
}
if (lastPayment) {
textRows.push(lastPayment)
}
if (nextPayment) {
textRows.push(nextPayment)
}
}
}
const setDaysRemainingInMonth = () => {
let now = new Date()
now = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const firstNextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1)
const difference = firstNextMonth.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 init = async () => {
try {
await licensing.getQuotaUsage()
@ -71,69 +200,99 @@
})
$: {
if (license && quotaUsage) {
setMonthlyUsage()
setStaticUsage()
if (license) {
setPrice()
setBalance()
setLastPayment()
setNextPayment()
setCancelAt()
setTextRows()
setDaysRemainingInMonth()
if (quotaUsage) {
setMonthlyUsage()
setStaticUsage()
}
}
}
</script>
{#if loaded}
<Layout>
<Heading>Usage</Heading>
<Body
>Get information about your current usage within Budibase.
{#if $admin.cloud}
{#if $auth.user?.accountPortalAccess}
To upgrade your plan and usage limits visit your <Link
size="L"
href={upgradeUrl}>Account</Link
>.
{:else}
Contact your account holder to upgrade your usage limits.
{/if}
{/if}
</Body>
</Layout>
<Layout gap="S">
<Divider size="S" />
</Layout>
<Layout gap="S" noPadding>
<Layout gap="XS">
<Body size="S">YOUR PLAN</Body>
<Heading size="S">{capitalise(license?.plan.type)}</Heading>
</Layout>
<Layout gap="S">
<Body size="S">USAGE</Body>
<div class="usages">
{#each staticUsage as usage}
<div class="usage">
<Usage {usage} />
</div>
{/each}
</div>
</Layout>
{#if monthlyUsage.length}
<Layout gap="S">
<Body size="S">MONTHLY</Body>
<div class="usages">
{#each monthlyUsage as usage}
<div class="usage">
<Usage {usage} />
</div>
{/each}
</div>
<Page maxWidth={"100ch"}>
{#if loaded}
<Layout>
<Layout noPadding gap="S">
<Heading>Billing</Heading>
<Body
>Get information about your current usage and manage your plan</Body
>
</Layout>
<div />
{/if}
</Layout>
{/if}
<Divider />
<DashCard
description="YOUR CURRENT PLAN"
title={planTitle()}
subtitle={planSubtitle()}
primaryActionText={cancelAt ? "Upgrade" : "Manage"}
primaryAction={goToAccountPortal}
{textRows}
>
<Layout gap="S" noPadding>
<Layout gap="S">
<div class="usages">
<Layout noPadding>
{#each staticUsage as usage}
<div class="usage">
<Usage
{usage}
warnWhenFull={warnUsage.includes(usage.name)}
/>
</div>
{/each}
</Layout>
</div>
</Layout>
{#if monthlyUsage.length}
<div class="monthly-container">
<Layout gap="S">
<Heading size="S" weight="light">Monthly</Heading>
<div class="detail">
<Detail size="M">Resets in {daysRemainingInMonth} days</Detail
>
</div>
<div class="usages">
<Layout noPadding>
{#each monthlyUsage as usage}
<div class="usage">
<Usage
{usage}
warnWhenFull={warnUsage.includes(usage.name)}
/>
</div>
{/each}
</Layout>
</div>
</Layout>
</div>
{/if}
</Layout>
</DashCard>
</Layout>
{/if}
</Page>
<style>
.usages {
display: grid;
column-gap: 60px;
row-gap: 50px;
grid-template-columns: 1fr 1fr 1fr;
display: flex;
flex-direction: column;
}
.detail :global(.spectrum-Detail) {
color: var(--spectrum-global-color-gray-700);
margin-bottom: 5px;
margin-top: -8px;
}
/*.monthly-container {*/
/* margin-top: -35px;*/
/*}*/
.card-container {
margin-top: 25px;
}
</style>