diff --git a/frontend/package.json b/frontend/package.json index 3a4649f7..065429c4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -30,6 +30,7 @@ "color": "^4.0.1", "copy-webpack-plugin": "^12.0.2", "css-loader": "^6.3.0", + "date-fns": "^3.6.0", "del-cli": "^4.0.1", "diff": "^5.2.0", "dotenv": "^10.0.0", diff --git a/frontend/src/features/index.ts b/frontend/src/features/index.ts index 3ae95bf9..3b0a50aa 100644 --- a/frontend/src/features/index.ts +++ b/frontend/src/features/index.ts @@ -3,4 +3,5 @@ import "./archived-items"; import "./browser-profiles"; import "./collections"; import "./crawl-workflows"; +import "./org"; import "./qa"; diff --git a/frontend/src/features/org/index.ts b/frontend/src/features/org/index.ts new file mode 100644 index 00000000..c0e6c1e5 --- /dev/null +++ b/frontend/src/features/org/index.ts @@ -0,0 +1 @@ +import("./org-status-banner"); diff --git a/frontend/src/features/org/org-status-banner.ts b/frontend/src/features/org/org-status-banner.ts new file mode 100644 index 00000000..ce74611b --- /dev/null +++ b/frontend/src/features/org/org-status-banner.ts @@ -0,0 +1,241 @@ +import { localized, msg, str } from "@lit/localize"; +import { differenceInDays } from "date-fns/fp"; +import { html, type PropertyValues, type TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; + +import { TailwindElement } from "@/classes/TailwindElement"; +import { NavigateController } from "@/controllers/navigate"; +import { OrgReadOnlyReason, type OrgData } from "@/types/org"; +import { formatISODateString } from "@/utils/localization"; +import appState, { use } from "@/utils/state"; + +type Alert = { + test: () => boolean; + persist?: boolean; + content: () => { + title: string | TemplateResult; + detail: string | TemplateResult; + }; +}; + +@localized() +@customElement("btrix-org-status-banner") +export class OrgStatusBanner extends TailwindElement { + @property({ type: Object }) + org?: OrgData; + + @use() + appState = appState; + + @state() + isAlertOpen = false; + + private readonly navigate = new NavigateController(this); + + private alert?: Alert; + + protected willUpdate(_changedProperties: PropertyValues): void { + if (_changedProperties.has("org") && this.org) { + this.alert = this.alerts.find(({ test }) => test()); + + if (this.alert) { + this.isAlertOpen = true; + } + } + } + + render() { + if (!this.org) return; + + return html` +
+
+ (this.isAlertOpen = false)} + > + + ${this.renderContent()} + +
+
+ `; + } + + private renderContent() { + if (!this.alert || !this.org) return; + + const content = this.alert.content(); + + return html` + ${content.title} + ${content.detail} + `; + } + + /** + * Alerts ordered by priority + */ + private get alerts(): Alert[] { + if (!this.org) return []; + + const billingTabLink = html`${msg("billing settings")}`; + + const { + readOnly, + readOnlyReason, + readOnlyOnCancel, + subscription, + storageQuotaReached, + execMinutesQuotaReached, + } = this.org; + + return [ + { + test: () => + !readOnly && !readOnlyOnCancel && !!subscription?.futureCancelDate, + persist: true, + content: () => { + const daysDiff = differenceInDays( + new Date(), + new Date(subscription!.futureCancelDate!), + ); + return { + title: + daysDiff > 1 + ? msg( + str`Your org will be deleted in + ${daysDiff} days`, + ) + : `Your org will be deleted within one day`, + detail: html` +

+ ${msg( + str`Your subscription ends on ${formatISODateString( + subscription!.futureCancelDate!, + { + month: "long", + day: "numeric", + year: "numeric", + hour: "numeric", + }, + )}. Your user account, org, and all associated data will be deleted.`, + )} +

+

+ ${msg( + html`We suggest downloading your archived items before they + are deleted. To keep your plan and data, see + ${billingTabLink}.`, + )} +

+ `, + }; + }, + }, + { + test: () => + !readOnly && readOnlyOnCancel && !!subscription?.futureCancelDate, + persist: true, + content: () => { + const daysDiff = differenceInDays( + new Date(), + new Date(subscription!.futureCancelDate!), + ); + return { + title: + daysDiff > 1 + ? msg( + str`Your org will be set to read-only mode in ${daysDiff} days`, + ) + : msg("Your org will be set to read-only mode within one day"), + detail: html` +

+ ${msg( + str`Your subscription ends on ${formatISODateString( + subscription!.futureCancelDate!, + { + month: "long", + day: "numeric", + year: "numeric", + hour: "numeric", + }, + )}. You will no longer be able to run crawls, upload files, create browser profiles, or create collections.`, + )} +

+

+ ${msg( + html`To keep your plan and continue crawling, see + ${billingTabLink}.`, + )} +

+ `, + }; + }, + }, + { + test: () => + !!readOnly && readOnlyReason === OrgReadOnlyReason.SubscriptionPaused, + persist: true, + content: () => ({ + title: msg(str`Your org has been set to read-only mode`), + detail: msg( + html`Your subscription has been paused due to payment failure. + Please go to ${billingTabLink} to update your payment method.`, + ), + }), + }, + { + test: () => + !!readOnly && + readOnlyReason === OrgReadOnlyReason.SubscriptionCancelled, + persist: true, + content: () => ({ + title: msg(str`This org has been set to read-only mode`), + detail: msg( + `Your subscription has been canceled. Please contact Browsertrix support to renew your plan.`, + ), + }), + }, + { + test: () => !!readOnly, + persist: true, + content: () => ({ + title: msg(str`This org has been set to read-only mode`), + detail: msg(`Please contact Browsertrix support to renew your plan.`), + }), + }, + { + test: () => !readOnly && !!storageQuotaReached, + content: () => ({ + title: msg(str`Your org has reached its storage limit`), + detail: msg( + str`To add archived items again, delete unneeded items and unused browser profiles to free up space, or contact ${this.appState.settings?.salesEmail || msg("Browsertrix host administrator")} to upgrade your storage plan.`, + ), + }), + }, + { + test: () => !readOnly && !!execMinutesQuotaReached, + content: () => ({ + title: msg( + str`Your org has reached its monthly execution minutes limit`, + ), + detail: msg( + str`Contact ${this.appState.settings?.salesEmail || msg("Browsertrix host administrator")} to purchase additional monthly execution minutes or upgrade your plan.`, + ), + }), + }, + ]; + } +} diff --git a/frontend/src/pages/org/index.ts b/frontend/src/pages/org/index.ts index d6add73f..78fecd81 100644 --- a/frontend/src/pages/org/index.ts +++ b/frontend/src/pages/org/index.ts @@ -116,6 +116,9 @@ export class Org extends LiteElement { @state() private orgStorageQuotaReached = false; + @state() + private showReadOnlyAlert = false; + @state() private showStorageQuotaAlert = false; @@ -238,6 +241,11 @@ export class Org extends LiteElement { if (!this.userInfo || !this.orgId) return; try { this.org = await this.getOrg(this.orgId); + + this.showReadOnlyAlert = Boolean( + this.org?.readOnly || this.org?.subscription?.futureCancelDate, + ); + this.checkStorageQuota(); this.checkExecutionMinutesQuota(); } catch { @@ -320,7 +328,7 @@ export class Org extends LiteElement { return html`
- ${this.renderStorageAlert()} ${this.renderExecutionMinutesAlert()} + ${this.renderOrgNavBar()}
-
- (this.showStorageQuotaAlert = false)} - > - - ${msg("Your org has reached its storage limit")}
- ${msg( - "To add archived items again, delete unneeded items and unused browser profiles to free up space, or contact us to upgrade your storage plan.", - )} -
-
-
- `; - } - - private renderExecutionMinutesAlert() { - return html` -
-
- - (this.showExecutionMinutesQuotaAlert = false)} - > - - ${msg( - "Your org has reached its monthly execution minutes limit", - )}
- ${msg( - "To purchase additional monthly execution minutes, contact us to upgrade your plan.", - )} -
-
-
- `; - } - private renderOrgNavBar() { return html`
= { superadmin: 100, viewer: 10, @@ -50,7 +55,8 @@ export type OrgData = { }; }; readOnly: boolean | null; - readOnlyReason: string | null; + readOnlyReason: OrgReadOnlyReason | string | null; + readOnlyOnCancel: boolean; subscription: null | Subscription; }; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 06ec59a7..3fceec59 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -3501,6 +3501,11 @@ data-uri-to-buffer@^4.0.0: resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== +date-fns@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.6.0.tgz#f20ca4fe94f8b754951b24240676e8618c0206bf" + integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww== + debounce@^1.2.0, debounce@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz"