- ${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.", - )} -
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` +
+ ${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`