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:
parent
7fa2b61b29
commit
96e48b001b
@ -1 +1,2 @@
|
||||
import("./org-status-banner");
|
||||
import("./usage-history-table");
|
||||
|
195
frontend/src/features/org/usage-history-table.ts
Normal file
195
frontend/src/features/org/usage-history-table.ts
Normal 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>
|
||||
`;
|
||||
}
|
||||
}
|
@ -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%`;
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user