From 96e48b001b9a9fb2c621a68015446bb2627069ad Mon Sep 17 00:00:00 2001 From: sua yoo Date: Tue, 6 Aug 2024 16:31:57 -0700 Subject: [PATCH] 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 --- frontend/src/features/org/index.ts | 1 + .../src/features/org/usage-history-table.ts | 195 ++++++++++++++++++ frontend/src/pages/org/dashboard.ts | 192 ++--------------- .../pages/org/settings/components/billing.ts | 27 ++- 4 files changed, 234 insertions(+), 181 deletions(-) create mode 100644 frontend/src/features/org/usage-history-table.ts diff --git a/frontend/src/features/org/index.ts b/frontend/src/features/org/index.ts index c0e6c1e5..e697ac84 100644 --- a/frontend/src/features/org/index.ts +++ b/frontend/src/features/org/index.ts @@ -1 +1,2 @@ import("./org-status-banner"); +import("./usage-history-table"); diff --git a/frontend/src/features/org/usage-history-table.ts b/frontend/src/features/org/usage-history-table.ts new file mode 100644 index 00000000..ef49d551 --- /dev/null +++ b/frontend/src/features/org/usage-history-table.ts @@ -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` +

+ ${msg("No usage history to show.")} +

+ `; + } + + const usageTableCols = [ + msg("Month"), + html` + ${msg("Elapsed Time")} + +
+ ${msg( + "Total duration of crawls and QA analysis runs, from start to finish", + )} +
+ +
+ `, + html` + ${msg("Execution Time")} + +
+ ${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", + )} +
+ +
+ `, + ]; + + if (this.hasMonthlyTime()) { + usageTableCols.push( + html`${msg("Billable Execution Time")} + +
+ ${msg( + "Execution time used that is billable to the current month of the plan", + )} +
+ +
`, + ); + } + if (this.hasExtraTime()) { + usageTableCols.push( + html`${msg("Rollover Execution Time")} + +
+ ${msg( + "Additional execution time used, of which any extra minutes will roll over to next month as billable time", + )} +
+ +
`, + ); + } + if (this.hasGiftedTime()) { + usageTableCols.push( + html`${msg("Gifted Execution Time")} + +
+ ${msg("Execution time used that is free of charge")} +
+ +
`, + ); + } + + 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` + + + `, + 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` + + `; + } +} diff --git a/frontend/src/pages/org/dashboard.ts b/frontend/src/pages/org/dashboard.ts index ab0b0203..c09db7f6 100644 --- a/frontend/src/pages/org/dashboard.ts +++ b/frontend/src/pages/org/dashboard.ts @@ -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 { )}
-
+
${this.renderCard( msg("Storage"), (metrics) => html` @@ -252,7 +251,21 @@ export class Dashboard extends LiteElement { `, )}
-
${this.renderUsageHistory()}
+ ${when( + this.appState.settings && + !this.appState.settings.billingEnabled && + this.org, + (org) => html` +
+ + ${msg("Usage History")} + + +
+ `, + )}
`; } @@ -667,179 +680,6 @@ export class Dashboard extends LiteElement { `; - 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")} - -
- ${msg("Total time elapsed between when crawls started and ended")} -
- -
- `, - html` - ${msg("Total Execution Time")} - -
- ${msg( - "Total billable time of all crawler instances this used month", - )} -
- -
- `, - ]; - - if (this.hasMonthlyTime()) { - usageTableCols.push( - html`${msg("Execution: Monthly")} - -
- ${msg("Billable time used, included with monthly plan")} -
- -
`, - ); - } - if (this.hasExtraTime()) { - usageTableCols.push( - html`${msg("Execution: Extra")} - -
- ${msg( - "Additional units of billable time used, any extra minutes will roll over to next month", - )} -
- -
`, - ); - } - if (this.hasGiftedTime()) { - usageTableCols.push( - html`${msg("Execution: Gifted")} - -
- ${msg( - "Usage of execution time added to your account free of charge", - )} -
- -
`, - ); - } - - 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` - - - `, - 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` - - ${msg("Usage History")} - - - `; - } - private renderPercentage(ratio: number) { const percent = ratio * 100; if (percent < 1) return `<1%`; diff --git a/frontend/src/pages/org/settings/components/billing.ts b/frontend/src/pages/org/settings/components/billing.ts index eb1a563e..0228e244 100644 --- a/frontend/src/pages/org/settings/components/billing.ts +++ b/frontend/src/pages/org/settings/components/billing.ts @@ -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 { ` : nothing} +
+ ${msg("Monthly quota")} +
${this.renderQuotas(org.quotas)} `, )} @@ -192,6 +200,15 @@ export class OrgSettingsBilling extends TailwindElement { `, ], ])} + +
+ +

${msg("Usage History")}

+
+ +
`; } @@ -263,7 +280,7 @@ export class OrgSettingsBilling extends TailwindElement {