feat: Update billing tab with usage & portal URL check (#1995)

- Hides usage table from dashboard if billing is enabled
- Shows usage table in billing settings
- Updates usage table column headings
- Fixes `portalUrl` task running unnecessarily
This commit is contained in:
sua yoo 2024-08-06 16:31:57 -07:00 committed by GitHub
parent 7fa2b61b29
commit 96e48b001b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 234 additions and 181 deletions

View File

@ -1 +1,2 @@
import("./org-status-banner");
import("./usage-history-table");

View File

@ -0,0 +1,195 @@
import { localized, msg } from "@lit/localize";
import { html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { TailwindElement } from "@/classes/TailwindElement";
import { humanizeExecutionSeconds } from "@/utils/executionTimeFormatter";
import { getLocale } from "@/utils/localization";
import type { OrgData, YearMonth } from "@/utils/orgs";
@localized()
@customElement("btrix-usage-history-table")
export class UsageHistoryTable extends TailwindElement {
@property({ type: Object })
org: OrgData | null = null;
private readonly hasMonthlyTime = () =>
this.org?.monthlyExecSeconds &&
Object.keys(this.org.monthlyExecSeconds).length;
private readonly hasExtraTime = () =>
this.org?.extraExecSeconds && Object.keys(this.org.extraExecSeconds).length;
private readonly hasGiftedTime = () =>
this.org?.giftedExecSeconds &&
Object.keys(this.org.giftedExecSeconds).length;
render() {
if (!this.org) return;
if (this.org.usage && !Object.keys(this.org.usage).length) {
return html`
<p class="text-center text-neutral-500">
${msg("No usage history to show.")}
</p>
`;
}
const usageTableCols = [
msg("Month"),
html`
${msg("Elapsed Time")}
<sl-tooltip>
<div slot="content" style="text-transform: initial">
${msg(
"Total duration of crawls and QA analysis runs, from start to finish",
)}
</div>
<sl-icon name="info-circle" style="vertical-align: -.175em"></sl-icon>
</sl-tooltip>
`,
html`
${msg("Execution Time")}
<sl-tooltip>
<div slot="content" style="text-transform: initial">
${msg(
"Aggregated time across all crawler instances that the crawler was actively executing a crawl or QA analysis run, i.e. not in a waiting state",
)}
</div>
<sl-icon name="info-circle" style="vertical-align: -.175em"></sl-icon>
</sl-tooltip>
`,
];
if (this.hasMonthlyTime()) {
usageTableCols.push(
html`${msg("Billable Execution Time")}
<sl-tooltip>
<div slot="content" style="text-transform: initial">
${msg(
"Execution time used that is billable to the current month of the plan",
)}
</div>
<sl-icon
name="info-circle"
style="vertical-align: -.175em"
></sl-icon>
</sl-tooltip>`,
);
}
if (this.hasExtraTime()) {
usageTableCols.push(
html`${msg("Rollover Execution Time")}
<sl-tooltip>
<div slot="content" style="text-transform: initial">
${msg(
"Additional execution time used, of which any extra minutes will roll over to next month as billable time",
)}
</div>
<sl-icon
name="info-circle"
style="vertical-align: -.175em"
></sl-icon>
</sl-tooltip>`,
);
}
if (this.hasGiftedTime()) {
usageTableCols.push(
html`${msg("Gifted Execution Time")}
<sl-tooltip>
<div slot="content" style="text-transform: initial">
${msg("Execution time used that is free of charge")}
</div>
<sl-icon
name="info-circle"
style="vertical-align: -.175em"
></sl-icon>
</sl-tooltip>`,
);
}
const rows = (Object.entries(this.org.usage || {}) as [YearMonth, number][])
// Sort latest
.reverse()
.map(([mY, crawlTime]) => {
if (!this.org) return [];
let monthlySecondsUsed = this.org.monthlyExecSeconds?.[mY] || 0;
let maxMonthlySeconds = 0;
if (this.org.quotas.maxExecMinutesPerMonth) {
maxMonthlySeconds = this.org.quotas.maxExecMinutesPerMonth * 60;
}
if (monthlySecondsUsed > maxMonthlySeconds) {
monthlySecondsUsed = maxMonthlySeconds;
}
let extraSecondsUsed = this.org.extraExecSeconds?.[mY] || 0;
let maxExtraSeconds = 0;
if (this.org.quotas.extraExecMinutes) {
maxExtraSeconds = this.org.quotas.extraExecMinutes * 60;
}
if (extraSecondsUsed > maxExtraSeconds) {
extraSecondsUsed = maxExtraSeconds;
}
let giftedSecondsUsed = this.org.giftedExecSeconds?.[mY] || 0;
let maxGiftedSeconds = 0;
if (this.org.quotas.giftedExecMinutes) {
maxGiftedSeconds = this.org.quotas.giftedExecMinutes * 60;
}
if (giftedSecondsUsed > maxGiftedSeconds) {
giftedSecondsUsed = maxGiftedSeconds;
}
let totalSecondsUsed = this.org.crawlExecSeconds?.[mY] || 0;
const totalMaxQuota =
maxMonthlySeconds + maxExtraSeconds + maxGiftedSeconds;
if (totalSecondsUsed > totalMaxQuota) {
totalSecondsUsed = totalMaxQuota;
}
const tableRows = [
html`
<sl-format-date
lang=${getLocale()}
date="${mY}-15T00:00:00.000Z"
time-zone="utc"
month="long"
year="numeric"
>
</sl-format-date>
`,
humanizeExecutionSeconds(crawlTime || 0),
totalSecondsUsed ? humanizeExecutionSeconds(totalSecondsUsed) : "--",
];
if (this.hasMonthlyTime()) {
tableRows.push(
monthlySecondsUsed
? humanizeExecutionSeconds(monthlySecondsUsed)
: "--",
);
}
if (this.hasExtraTime()) {
tableRows.push(
extraSecondsUsed
? humanizeExecutionSeconds(extraSecondsUsed)
: "--",
);
}
if (this.hasGiftedTime()) {
tableRows.push(
giftedSecondsUsed
? humanizeExecutionSeconds(giftedSecondsUsed)
: "--",
);
}
return tableRows;
});
return html`
<btrix-data-table
.columns=${usageTableCols}
.rows=${rows}
></btrix-data-table>
`;
}
}

View File

@ -10,7 +10,6 @@ import type { SelectNewDialogEvent } from ".";
import type { AuthState } from "@/utils/AuthService";
import { humanizeExecutionSeconds } from "@/utils/executionTimeFormatter";
import LiteElement, { html } from "@/utils/LiteElement";
import { getLocale } from "@/utils/localization";
import type { OrgData, YearMonth } from "@/utils/orgs";
type Metrics = {
@ -137,7 +136,7 @@ export class Dashboard extends LiteElement {
)}
</header>
<main>
<div class="flex flex-col gap-6 md:flex-row">
<div class="mb-10 flex flex-col gap-6 md:flex-row">
${this.renderCard(
msg("Storage"),
(metrics) => html`
@ -252,7 +251,21 @@ export class Dashboard extends LiteElement {
`,
)}
</div>
<section class="mt-10">${this.renderUsageHistory()}</section>
${when(
this.appState.settings &&
!this.appState.settings.billingEnabled &&
this.org,
(org) => html`
<section class="mb-10">
<btrix-details open>
<span slot="title">${msg("Usage History")}</span>
<btrix-usage-history-table
.org=${org}
></btrix-usage-history-table>
</btrix-details>
</section>
`,
)}
</main> `;
}
@ -667,179 +680,6 @@ export class Dashboard extends LiteElement {
<sl-skeleton class="mb-3" effect="sheen"></sl-skeleton>
`;
private readonly hasMonthlyTime = () =>
this.org?.monthlyExecSeconds &&
Object.keys(this.org.monthlyExecSeconds).length;
private readonly hasExtraTime = () =>
this.org?.extraExecSeconds && Object.keys(this.org.extraExecSeconds).length;
private readonly hasGiftedTime = () =>
this.org?.giftedExecSeconds &&
Object.keys(this.org.giftedExecSeconds).length;
private renderUsageHistory() {
if (!this.org) return;
const usageTableCols = [
msg("Month"),
html`
${msg("Elapsed Time")}
<sl-tooltip>
<div slot="content" style="text-transform: initial">
${msg("Total time elapsed between when crawls started and ended")}
</div>
<sl-icon name="info-circle" style="vertical-align: -.175em"></sl-icon>
</sl-tooltip>
`,
html`
${msg("Total Execution Time")}
<sl-tooltip>
<div slot="content" style="text-transform: initial">
${msg(
"Total billable time of all crawler instances this used month",
)}
</div>
<sl-icon name="info-circle" style="vertical-align: -.175em"></sl-icon>
</sl-tooltip>
`,
];
if (this.hasMonthlyTime()) {
usageTableCols.push(
html`${msg("Execution: Monthly")}
<sl-tooltip>
<div slot="content" style="text-transform: initial">
${msg("Billable time used, included with monthly plan")}
</div>
<sl-icon
name="info-circle"
style="vertical-align: -.175em"
></sl-icon>
</sl-tooltip>`,
);
}
if (this.hasExtraTime()) {
usageTableCols.push(
html`${msg("Execution: Extra")}
<sl-tooltip>
<div slot="content" style="text-transform: initial">
${msg(
"Additional units of billable time used, any extra minutes will roll over to next month",
)}
</div>
<sl-icon
name="info-circle"
style="vertical-align: -.175em"
></sl-icon>
</sl-tooltip>`,
);
}
if (this.hasGiftedTime()) {
usageTableCols.push(
html`${msg("Execution: Gifted")}
<sl-tooltip>
<div slot="content" style="text-transform: initial">
${msg(
"Usage of execution time added to your account free of charge",
)}
</div>
<sl-icon
name="info-circle"
style="vertical-align: -.175em"
></sl-icon>
</sl-tooltip>`,
);
}
const rows = (Object.entries(this.org.usage || {}) as [YearMonth, number][])
// Sort latest
.reverse()
.map(([mY, crawlTime]) => {
if (!this.org) return [];
let monthlySecondsUsed = this.org.monthlyExecSeconds?.[mY] || 0;
let maxMonthlySeconds = 0;
if (this.org.quotas.maxExecMinutesPerMonth) {
maxMonthlySeconds = this.org.quotas.maxExecMinutesPerMonth * 60;
}
if (monthlySecondsUsed > maxMonthlySeconds) {
monthlySecondsUsed = maxMonthlySeconds;
}
let extraSecondsUsed = this.org.extraExecSeconds?.[mY] || 0;
let maxExtraSeconds = 0;
if (this.org.quotas.extraExecMinutes) {
maxExtraSeconds = this.org.quotas.extraExecMinutes * 60;
}
if (extraSecondsUsed > maxExtraSeconds) {
extraSecondsUsed = maxExtraSeconds;
}
let giftedSecondsUsed = this.org.giftedExecSeconds?.[mY] || 0;
let maxGiftedSeconds = 0;
if (this.org.quotas.giftedExecMinutes) {
maxGiftedSeconds = this.org.quotas.giftedExecMinutes * 60;
}
if (giftedSecondsUsed > maxGiftedSeconds) {
giftedSecondsUsed = maxGiftedSeconds;
}
let totalSecondsUsed = this.org.crawlExecSeconds?.[mY] || 0;
const totalMaxQuota =
maxMonthlySeconds + maxExtraSeconds + maxGiftedSeconds;
if (totalSecondsUsed > totalMaxQuota) {
totalSecondsUsed = totalMaxQuota;
}
const tableRows = [
html`
<sl-format-date
lang=${getLocale()}
date="${mY}-15T00:00:00.000Z"
time-zone="utc"
month="long"
year="numeric"
>
</sl-format-date>
`,
humanizeExecutionSeconds(crawlTime || 0),
totalSecondsUsed ? humanizeExecutionSeconds(totalSecondsUsed) : "--",
];
if (this.hasMonthlyTime()) {
tableRows.push(
monthlySecondsUsed
? humanizeExecutionSeconds(monthlySecondsUsed)
: "--",
);
}
if (this.hasExtraTime()) {
tableRows.push(
extraSecondsUsed
? humanizeExecutionSeconds(extraSecondsUsed)
: "--",
);
}
if (this.hasGiftedTime()) {
tableRows.push(
giftedSecondsUsed
? humanizeExecutionSeconds(giftedSecondsUsed)
: "--",
);
}
return tableRows;
});
return html`
<btrix-details>
<span slot="title">${msg("Usage History")}</span>
<btrix-data-table
.columns=${usageTableCols}
.rows=${rows}
></btrix-data-table>
</btrix-details>
`;
}
private renderPercentage(ratio: number) {
const percent = ratio * 100;
if (percent < 1) return `<1%`;

View File

@ -74,10 +74,15 @@ export class OrgSettingsBilling extends TailwindElement {
}
private readonly portalUrl = new Task(this, {
task: async ([org, authState]) => {
if (!org || !authState) throw new Error("Missing args");
task: async ([appState]) => {
if (!appState.settings?.billingEnabled || !appState.org?.subscription)
return;
try {
const { portalUrl } = await this.getPortalUrl(org.id, authState);
const { portalUrl } = await this.getPortalUrl(
appState.org.id,
this.authState!,
);
if (portalUrl) {
return portalUrl;
@ -92,7 +97,7 @@ export class OrgSettingsBilling extends TailwindElement {
);
}
},
args: () => [this.org, this.authState] as const,
args: () => [this.appState] as const,
});
render() {
@ -148,6 +153,9 @@ export class OrgSettingsBilling extends TailwindElement {
</div>
`
: nothing}
<h5 class="mb-2 mt-4 text-xs leading-none text-neutral-500">
${msg("Monthly quota")}
</h5>
${this.renderQuotas(org.quotas)}
`,
)}
@ -192,6 +200,15 @@ export class OrgSettingsBilling extends TailwindElement {
`,
],
])}
<div class="p-4">
<btrix-section-heading style="--margin: var(--sl-spacing-medium)">
<h4>${msg("Usage History")}</h4>
</btrix-section-heading>
<btrix-usage-history-table
.org=${ifDefined(this.org)}
></btrix-usage-history-table>
</div>
</div>
`;
}
@ -263,7 +280,7 @@ export class OrgSettingsBilling extends TailwindElement {
<ul class="leading-relaxed text-neutral-700">
<li>
${msg(
str`${quotas.maxExecMinutesPerMonth ? humanizeSeconds(quotas.maxExecMinutesPerMonth * 60, undefined, undefined, "long") : msg("Unlimited minutes")} of crawling and QA analysis time per month`,
str`${quotas.maxExecMinutesPerMonth ? humanizeSeconds(quotas.maxExecMinutesPerMonth * 60, undefined, undefined, "long") : msg("Unlimited minutes")} of crawl and QA analysis execution time`,
)}
</li>
<li>