Usage page updates WIP
This commit is contained in:
parent
cb03d26980
commit
d4d542e773
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -0,0 +1,2 @@
|
|||
export { default as Usage } from "./Usage.svelte"
|
||||
export { default as DashCard } from "./UsageDashCard.svelte"
|
|
@ -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",
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
if (license) {
|
||||
setPrice()
|
||||
setBalance()
|
||||
setLastPayment()
|
||||
setNextPayment()
|
||||
setCancelAt()
|
||||
setTextRows()
|
||||
setDaysRemainingInMonth()
|
||||
|
||||
if (quotaUsage) {
|
||||
setMonthlyUsage()
|
||||
setStaticUsage()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if loaded}
|
||||
<Page maxWidth={"100ch"}>
|
||||
{#if loaded}
|
||||
<Layout>
|
||||
<Heading>Usage</Heading>
|
||||
<Layout noPadding gap="S">
|
||||
<Heading>Billing</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" />
|
||||
>Get information about your current usage and manage your plan</Body
|
||||
>
|
||||
</Layout>
|
||||
<Divider />
|
||||
<DashCard
|
||||
description="YOUR CURRENT PLAN"
|
||||
title={planTitle()}
|
||||
subtitle={planSubtitle()}
|
||||
primaryActionText={cancelAt ? "Upgrade" : "Manage"}
|
||||
primaryAction={goToAccountPortal}
|
||||
{textRows}
|
||||
>
|
||||
<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">
|
||||
<Layout noPadding>
|
||||
{#each staticUsage as usage}
|
||||
<div class="usage">
|
||||
<Usage {usage} />
|
||||
<Usage
|
||||
{usage}
|
||||
warnWhenFull={warnUsage.includes(usage.name)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</Layout>
|
||||
</div>
|
||||
</Layout>
|
||||
{#if monthlyUsage.length}
|
||||
<div class="monthly-container">
|
||||
<Layout gap="S">
|
||||
<Body size="S">MONTHLY</Body>
|
||||
<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} />
|
||||
<Usage
|
||||
{usage}
|
||||
warnWhenFull={warnUsage.includes(usage.name)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</Layout>
|
||||
</div>
|
||||
</Layout>
|
||||
<div />
|
||||
</div>
|
||||
{/if}
|
||||
</Layout>
|
||||
{/if}
|
||||
</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>
|
||||
|
|
Loading…
Reference in New Issue