Usage page updates WIP
This commit is contained in:
parent
8fc8308530
commit
fe09208bb1
|
@ -1,10 +1,13 @@
|
||||||
<script>
|
<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"
|
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
|
||||||
|
|
||||||
const isUnlimited = () => {
|
const isUnlimited = () => {
|
||||||
if (usage.total === -1) {
|
if (usage.total === -1) {
|
||||||
|
@ -14,29 +17,57 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
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 queries <Link href={upgradeUrl}>upgrade your plan</Link>
|
||||||
|
</Body>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -51,6 +82,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,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)",
|
navBackground: "var(--spectrum-global-color-gray-50)",
|
||||||
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",
|
||||||
|
}
|
||||||
|
|
|
@ -5,20 +5,39 @@
|
||||||
Heading,
|
Heading,
|
||||||
Layout,
|
Layout,
|
||||||
notifications,
|
notifications,
|
||||||
Link,
|
Page,
|
||||||
|
Detail,
|
||||||
} 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 price
|
||||||
|
let lastPayment
|
||||||
|
let cancelAt
|
||||||
|
let nextPayment
|
||||||
|
let balance
|
||||||
let loaded = false
|
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
|
$: quotaUsage = $licensing.quotaUsage
|
||||||
$: license = $auth.user?.license
|
$: 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 = () => {
|
const setMonthlyUsage = () => {
|
||||||
monthlyUsage = []
|
monthlyUsage = []
|
||||||
|
@ -34,6 +53,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
monthlyUsage = monthlyUsage.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
const setStaticUsage = () => {
|
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 => {
|
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 () => {
|
const init = async () => {
|
||||||
try {
|
try {
|
||||||
await licensing.getQuotaUsage()
|
await licensing.getQuotaUsage()
|
||||||
|
@ -71,69 +200,99 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (license && quotaUsage) {
|
if (license) {
|
||||||
setMonthlyUsage()
|
setPrice()
|
||||||
setStaticUsage()
|
setBalance()
|
||||||
|
setLastPayment()
|
||||||
|
setNextPayment()
|
||||||
|
setCancelAt()
|
||||||
|
setTextRows()
|
||||||
|
setDaysRemainingInMonth()
|
||||||
|
|
||||||
|
if (quotaUsage) {
|
||||||
|
setMonthlyUsage()
|
||||||
|
setStaticUsage()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if loaded}
|
<Page maxWidth={"100ch"}>
|
||||||
<Layout>
|
{#if loaded}
|
||||||
<Heading>Usage</Heading>
|
<Layout>
|
||||||
<Body
|
<Layout noPadding gap="S">
|
||||||
>Get information about your current usage within Budibase.
|
<Heading>Billing</Heading>
|
||||||
{#if $admin.cloud}
|
<Body
|
||||||
{#if $auth.user?.accountPortalAccess}
|
>Get information about your current usage and manage your plan</Body
|
||||||
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>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
<div />
|
<Divider />
|
||||||
{/if}
|
<DashCard
|
||||||
</Layout>
|
description="YOUR CURRENT PLAN"
|
||||||
{/if}
|
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>
|
<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);
|
||||||
|
margin-bottom: 5px;
|
||||||
|
margin-top: -8px;
|
||||||
|
}
|
||||||
|
/*.monthly-container {*/
|
||||||
|
/* margin-top: -35px;*/
|
||||||
|
/*}*/
|
||||||
|
.card-container {
|
||||||
|
margin-top: 25px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
Loading…
Reference in New Issue