fix: Update org status banner on quota reached (#1956)

Fixes https://github.com/webrecorder/browsertrix/issues/1954

### Changes

- Refactors app state to include org data
- Fixes banner not showing if storage or execution minutes is exceeded
after page load
- Disables closing banners
- Refreshes org when tab changes

---------
Co-authored-by: emma <hi@emma.cafe>
This commit is contained in:
sua yoo 2024-07-23 22:55:45 -07:00 committed by GitHub
parent b35669af8d
commit 08147ec77d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 147 additions and 172 deletions

View File

@ -1,17 +1,16 @@
import { localized, msg, str } from "@lit/localize"; import { localized, msg, str } from "@lit/localize";
import { differenceInDays } from "date-fns/fp"; import { differenceInDays } from "date-fns/fp";
import { html, type PropertyValues, type TemplateResult } from "lit"; import { html, type TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators.js"; import { customElement } from "lit/decorators.js";
import { TailwindElement } from "@/classes/TailwindElement"; import { TailwindElement } from "@/classes/TailwindElement";
import { NavigateController } from "@/controllers/navigate"; import { NavigateController } from "@/controllers/navigate";
import { OrgReadOnlyReason, type OrgData } from "@/types/org"; import { OrgReadOnlyReason } from "@/types/org";
import { formatISODateString } from "@/utils/localization"; import { formatISODateString } from "@/utils/localization";
import appState, { use } from "@/utils/state"; import appState, { use } from "@/utils/state";
type Alert = { type Alert = {
test: () => boolean; test: () => boolean;
persist?: boolean;
content: () => { content: () => {
title: string | TemplateResult; title: string | TemplateResult;
detail: string | TemplateResult; detail: string | TemplateResult;
@ -21,64 +20,37 @@ type Alert = {
@localized() @localized()
@customElement("btrix-org-status-banner") @customElement("btrix-org-status-banner")
export class OrgStatusBanner extends TailwindElement { export class OrgStatusBanner extends TailwindElement {
@property({ type: Object })
org?: OrgData;
@use() @use()
appState = appState; appState = appState;
@state()
isAlertOpen = false;
private readonly navigate = new NavigateController(this); private readonly navigate = new NavigateController(this);
private alert?: Alert; private get org() {
return this.appState.org;
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() { render() {
if (!this.org) return; if (!this.org) return;
const alert = this.alerts.find(({ test }) => test());
if (!alert) return;
const content = alert.content();
return html` return html`
<div <div id="banner" class="border-b bg-slate-100 py-5">
class="${this.isAlertOpen
? "bg-slate-100 border-b py-5"
: ""} transition-all"
>
<div class="mx-auto box-border w-full max-w-screen-desktop px-3"> <div class="mx-auto box-border w-full max-w-screen-desktop px-3">
<sl-alert <sl-alert variant="danger" open>
variant="danger"
?closable=${!this.alert?.persist}
?open=${this.isAlertOpen}
@sl-after-hide=${() => (this.isAlertOpen = false)}
>
<sl-icon slot="icon" name="exclamation-triangle-fill"></sl-icon> <sl-icon slot="icon" name="exclamation-triangle-fill"></sl-icon>
${this.renderContent()} <strong class="block font-semibold">${content.title}</strong>
${content.detail}
</sl-alert> </sl-alert>
</div> </div>
</div> </div>
`; `;
} }
private renderContent() {
if (!this.alert || !this.org) return;
const content = this.alert.content();
return html`
<strong class="block font-semibold">${content.title}</strong>
${content.detail}
`;
}
/** /**
* Alerts ordered by priority * Alerts ordered by priority
*/ */
@ -105,7 +77,7 @@ export class OrgStatusBanner extends TailwindElement {
{ {
test: () => test: () =>
!readOnly && !readOnlyOnCancel && !!subscription?.futureCancelDate, !readOnly && !readOnlyOnCancel && !!subscription?.futureCancelDate,
persist: true,
content: () => { content: () => {
const daysDiff = differenceInDays( const daysDiff = differenceInDays(
new Date(), new Date(),
@ -147,7 +119,7 @@ export class OrgStatusBanner extends TailwindElement {
{ {
test: () => test: () =>
!readOnly && readOnlyOnCancel && !!subscription?.futureCancelDate, !readOnly && readOnlyOnCancel && !!subscription?.futureCancelDate,
persist: true,
content: () => { content: () => {
const daysDiff = differenceInDays( const daysDiff = differenceInDays(
new Date(), new Date(),
@ -187,7 +159,7 @@ export class OrgStatusBanner extends TailwindElement {
{ {
test: () => test: () =>
!!readOnly && readOnlyReason === OrgReadOnlyReason.SubscriptionPaused, !!readOnly && readOnlyReason === OrgReadOnlyReason.SubscriptionPaused,
persist: true,
content: () => ({ content: () => ({
title: msg(str`Your org has been set to read-only mode`), title: msg(str`Your org has been set to read-only mode`),
detail: msg( detail: msg(
@ -200,7 +172,7 @@ export class OrgStatusBanner extends TailwindElement {
test: () => test: () =>
!!readOnly && !!readOnly &&
readOnlyReason === OrgReadOnlyReason.SubscriptionCancelled, readOnlyReason === OrgReadOnlyReason.SubscriptionCancelled,
persist: true,
content: () => ({ content: () => ({
title: msg(str`This org has been set to read-only mode`), title: msg(str`This org has been set to read-only mode`),
detail: msg( detail: msg(
@ -210,7 +182,7 @@ export class OrgStatusBanner extends TailwindElement {
}, },
{ {
test: () => !!readOnly, test: () => !!readOnly,
persist: true,
content: () => ({ content: () => ({
title: msg(str`This org has been set to read-only mode`), title: msg(str`This org has been set to read-only mode`),
detail: msg(`Please contact Browsertrix support to renew your plan.`), detail: msg(`Please contact Browsertrix support to renew your plan.`),

View File

@ -84,7 +84,7 @@ export class Dashboard extends LiteElement {
() => () =>
html` <sl-icon-button html` <sl-icon-button
href=${`${this.orgBasePath}/settings`} href=${`${this.orgBasePath}/settings`}
class="text-lg" class="size-8 text-lg"
name="gear" name="gear"
label=${msg("Edit org settings")} label=${msg("Edit org settings")}
@click=${this.navLink} @click=${this.navLink}
@ -355,24 +355,26 @@ export class Dashboard extends LiteElement {
} }
private renderCrawlingMeter(_metrics: Metrics) { private renderCrawlingMeter(_metrics: Metrics) {
if (!this.org) return;
let quotaSeconds = 0; let quotaSeconds = 0;
if (this.org!.quotas.maxExecMinutesPerMonth) { if (this.org.quotas.maxExecMinutesPerMonth) {
quotaSeconds = this.org!.quotas.maxExecMinutesPerMonth * 60; quotaSeconds = this.org.quotas.maxExecMinutesPerMonth * 60;
} }
let quotaSecondsAllTypes = quotaSeconds; let quotaSecondsAllTypes = quotaSeconds;
let quotaSecondsExtra = 0; let quotaSecondsExtra = 0;
if (this.org!.extraExecSecondsAvailable) { if (this.org.extraExecSecondsAvailable) {
quotaSecondsExtra = this.org!.extraExecSecondsAvailable; quotaSecondsExtra = this.org.extraExecSecondsAvailable;
quotaSecondsAllTypes += this.org!.extraExecSecondsAvailable; quotaSecondsAllTypes += this.org.extraExecSecondsAvailable;
} }
let quotaSecondsGifted = 0; let quotaSecondsGifted = 0;
if (this.org!.giftedExecSecondsAvailable) { if (this.org.giftedExecSecondsAvailable) {
quotaSecondsGifted = this.org!.giftedExecSecondsAvailable; quotaSecondsGifted = this.org.giftedExecSecondsAvailable;
quotaSecondsAllTypes += this.org!.giftedExecSecondsAvailable; quotaSecondsAllTypes += this.org.giftedExecSecondsAvailable;
} }
const now = new Date(); const now = new Date();
@ -381,8 +383,8 @@ export class Dashboard extends LiteElement {
const currentPeriod = `${currentYear}-${currentMonth}` as YearMonth; const currentPeriod = `${currentYear}-${currentMonth}` as YearMonth;
let usageSeconds = 0; let usageSeconds = 0;
if (this.org!.monthlyExecSeconds) { if (this.org.monthlyExecSeconds) {
const actualUsage = this.org!.monthlyExecSeconds[currentPeriod]; const actualUsage = this.org.monthlyExecSeconds[currentPeriod];
if (actualUsage) { if (actualUsage) {
usageSeconds = actualUsage; usageSeconds = actualUsage;
} }
@ -393,21 +395,21 @@ export class Dashboard extends LiteElement {
} }
let usageSecondsAllTypes = 0; let usageSecondsAllTypes = 0;
if (this.org!.crawlExecSeconds) { if (this.org.crawlExecSeconds) {
const actualUsage = this.org!.crawlExecSeconds[currentPeriod]; const actualUsage = this.org.crawlExecSeconds[currentPeriod];
if (actualUsage) { if (actualUsage) {
usageSecondsAllTypes = actualUsage; usageSecondsAllTypes = actualUsage;
} }
} }
let usageSecondsExtra = 0; let usageSecondsExtra = 0;
if (this.org!.extraExecSeconds) { if (this.org.extraExecSeconds) {
const actualUsageExtra = this.org!.extraExecSeconds[currentPeriod]; const actualUsageExtra = this.org.extraExecSeconds[currentPeriod];
if (actualUsageExtra) { if (actualUsageExtra) {
usageSecondsExtra = actualUsageExtra; usageSecondsExtra = actualUsageExtra;
} }
} }
const maxExecSecsExtra = this.org!.quotas.extraExecMinutes * 60; const maxExecSecsExtra = this.org.quotas.extraExecMinutes * 60;
// Cap usage at quota for display purposes // Cap usage at quota for display purposes
if (usageSecondsExtra > maxExecSecsExtra) { if (usageSecondsExtra > maxExecSecsExtra) {
usageSecondsExtra = maxExecSecsExtra; usageSecondsExtra = maxExecSecsExtra;
@ -419,13 +421,13 @@ export class Dashboard extends LiteElement {
} }
let usageSecondsGifted = 0; let usageSecondsGifted = 0;
if (this.org!.giftedExecSeconds) { if (this.org.giftedExecSeconds) {
const actualUsageGifted = this.org!.giftedExecSeconds[currentPeriod]; const actualUsageGifted = this.org.giftedExecSeconds[currentPeriod];
if (actualUsageGifted) { if (actualUsageGifted) {
usageSecondsGifted = actualUsageGifted; usageSecondsGifted = actualUsageGifted;
} }
} }
const maxExecSecsGifted = this.org!.quotas.giftedExecMinutes * 60; const maxExecSecsGifted = this.org.quotas.giftedExecMinutes * 60;
// Cap usage at quota for display purposes // Cap usage at quota for display purposes
if (usageSecondsGifted > maxExecSecsGifted) { if (usageSecondsGifted > maxExecSecsGifted) {
usageSecondsGifted = maxExecSecsGifted; usageSecondsGifted = maxExecSecsGifted;
@ -447,9 +449,9 @@ export class Dashboard extends LiteElement {
const hasExtra = const hasExtra =
usageSecondsExtra || usageSecondsExtra ||
this.org!.extraExecSecondsAvailable || this.org.extraExecSecondsAvailable ||
usageSecondsGifted || usageSecondsGifted ||
this.org!.giftedExecSecondsAvailable; this.org.giftedExecSecondsAvailable;
const renderBar = ( const renderBar = (
/** Time in Seconds */ /** Time in Seconds */
@ -502,14 +504,14 @@ export class Dashboard extends LiteElement {
</div> </div>
`, `,
() => () =>
hasQuota hasQuota && this.org
? html` ? html`
<span class="inline-flex items-center"> <span class="inline-flex items-center">
${humanizeExecutionSeconds( ${humanizeExecutionSeconds(
quotaSeconds - quotaSeconds -
usageSeconds + usageSeconds +
this.org!.extraExecSecondsAvailable + this.org.extraExecSecondsAvailable +
this.org!.giftedExecSecondsAvailable, this.org.giftedExecSecondsAvailable,
{ style: "short", round: "down" }, { style: "short", round: "down" },
)} )}
<span class="ml-1">${msg("remaining")}</span> <span class="ml-1">${msg("remaining")}</span>
@ -519,12 +521,12 @@ export class Dashboard extends LiteElement {
)} )}
</div> </div>
${when( ${when(
hasQuota, hasQuota && this.org,
() => html` (org) => html`
<div class="mb-2"> <div class="mb-2">
<btrix-meter <btrix-meter
value=${this.org!.giftedExecSecondsAvailable || value=${org.giftedExecSecondsAvailable ||
this.org!.extraExecSecondsAvailable || org.extraExecSecondsAvailable ||
isReached isReached
? quotaSecondsAllTypes ? quotaSecondsAllTypes
: usageSeconds} : usageSeconds}
@ -540,9 +542,7 @@ export class Dashboard extends LiteElement {
hasExtra ? true : false, hasExtra ? true : false,
), ),
)} )}
${when( ${when(usageSecondsGifted || org.giftedExecSecondsAvailable, () =>
usageSecondsGifted || this.org!.giftedExecSecondsAvailable,
() =>
renderBar( renderBar(
usageSecondsGifted > quotaSecondsGifted usageSecondsGifted > quotaSecondsGifted
? quotaSecondsGifted ? quotaSecondsGifted
@ -552,9 +552,7 @@ export class Dashboard extends LiteElement {
"blue", "blue",
), ),
)} )}
${when( ${when(usageSecondsExtra || org.extraExecSecondsAvailable, () =>
usageSecondsExtra || this.org!.extraExecSecondsAvailable,
() =>
renderBar( renderBar(
usageSecondsExtra > quotaSecondsExtra usageSecondsExtra > quotaSecondsExtra
? quotaSecondsExtra ? quotaSecondsExtra
@ -664,13 +662,15 @@ export class Dashboard extends LiteElement {
`; `;
private readonly hasMonthlyTime = () => private readonly hasMonthlyTime = () =>
Object.keys(this.org!.monthlyExecSeconds!).length; this.org?.monthlyExecSeconds &&
Object.keys(this.org.monthlyExecSeconds).length;
private readonly hasExtraTime = () => private readonly hasExtraTime = () =>
Object.keys(this.org!.extraExecSeconds!).length; this.org?.extraExecSeconds && Object.keys(this.org.extraExecSeconds).length;
private readonly hasGiftedTime = () => private readonly hasGiftedTime = () =>
Object.keys(this.org!.giftedExecSeconds!).length; this.org?.giftedExecSeconds &&
Object.keys(this.org.giftedExecSeconds).length;
private renderUsageHistory() { private renderUsageHistory() {
if (!this.org) return; if (!this.org) return;
@ -750,34 +750,36 @@ export class Dashboard extends LiteElement {
// Sort latest // Sort latest
.reverse() .reverse()
.map(([mY, crawlTime]) => { .map(([mY, crawlTime]) => {
let monthlySecondsUsed = this.org!.monthlyExecSeconds?.[mY] || 0; if (!this.org) return [];
let monthlySecondsUsed = this.org.monthlyExecSeconds?.[mY] || 0;
let maxMonthlySeconds = 0; let maxMonthlySeconds = 0;
if (this.org!.quotas.maxExecMinutesPerMonth) { if (this.org.quotas.maxExecMinutesPerMonth) {
maxMonthlySeconds = this.org!.quotas.maxExecMinutesPerMonth * 60; maxMonthlySeconds = this.org.quotas.maxExecMinutesPerMonth * 60;
} }
if (monthlySecondsUsed > maxMonthlySeconds) { if (monthlySecondsUsed > maxMonthlySeconds) {
monthlySecondsUsed = maxMonthlySeconds; monthlySecondsUsed = maxMonthlySeconds;
} }
let extraSecondsUsed = this.org!.extraExecSeconds?.[mY] || 0; let extraSecondsUsed = this.org.extraExecSeconds?.[mY] || 0;
let maxExtraSeconds = 0; let maxExtraSeconds = 0;
if (this.org!.quotas.extraExecMinutes) { if (this.org.quotas.extraExecMinutes) {
maxExtraSeconds = this.org!.quotas.extraExecMinutes * 60; maxExtraSeconds = this.org.quotas.extraExecMinutes * 60;
} }
if (extraSecondsUsed > maxExtraSeconds) { if (extraSecondsUsed > maxExtraSeconds) {
extraSecondsUsed = maxExtraSeconds; extraSecondsUsed = maxExtraSeconds;
} }
let giftedSecondsUsed = this.org!.giftedExecSeconds?.[mY] || 0; let giftedSecondsUsed = this.org.giftedExecSeconds?.[mY] || 0;
let maxGiftedSeconds = 0; let maxGiftedSeconds = 0;
if (this.org!.quotas.giftedExecMinutes) { if (this.org.quotas.giftedExecMinutes) {
maxGiftedSeconds = this.org!.quotas.giftedExecMinutes * 60; maxGiftedSeconds = this.org.quotas.giftedExecMinutes * 60;
} }
if (giftedSecondsUsed > maxGiftedSeconds) { if (giftedSecondsUsed > maxGiftedSeconds) {
giftedSecondsUsed = maxGiftedSeconds; giftedSecondsUsed = maxGiftedSeconds;
} }
let totalSecondsUsed = this.org!.crawlExecSeconds?.[mY] || 0; let totalSecondsUsed = this.org.crawlExecSeconds?.[mY] || 0;
const totalMaxQuota = const totalMaxQuota =
maxMonthlySeconds + maxExtraSeconds + maxGiftedSeconds; maxMonthlySeconds + maxExtraSeconds + maxGiftedSeconds;
if (totalSecondsUsed > totalMaxQuota) { if (totalSecondsUsed > totalMaxQuota) {

View File

@ -24,6 +24,7 @@ import type { AuthState } from "@/utils/AuthService";
import { DEFAULT_MAX_SCALE } from "@/utils/crawler"; import { DEFAULT_MAX_SCALE } from "@/utils/crawler";
import LiteElement, { html } from "@/utils/LiteElement"; import LiteElement, { html } from "@/utils/LiteElement";
import { isAdmin, isCrawler, type OrgData } from "@/utils/orgs"; import { isAdmin, isCrawler, type OrgData } from "@/utils/orgs";
import { AppStateService } from "@/utils/state";
import "./workflow-detail"; import "./workflow-detail";
import "./workflows-list"; import "./workflows-list";
@ -113,30 +114,12 @@ export class Org extends LiteElement {
@property({ type: Number }) @property({ type: Number })
maxScale: number = DEFAULT_MAX_SCALE; maxScale: number = DEFAULT_MAX_SCALE;
@state()
private orgStorageQuotaReached = false;
@state()
private showReadOnlyAlert = false;
@state()
private showStorageQuotaAlert = false;
@state()
private orgExecutionMinutesQuotaReached = false;
@state()
private showExecutionMinutesQuotaAlert = false;
@state() @state()
private openDialogName?: ResourceName; private openDialogName?: ResourceName;
@state() @state()
private isCreateDialogVisible = false; private isCreateDialogVisible = false;
@state()
private org?: OrgData | null;
get userOrg() { get userOrg() {
if (!this.userInfo) return null; if (!this.userInfo) return null;
return this.userInfo.orgs.find(({ slug }) => slug === this.slug)!; return this.userInfo.orgs.find(({ slug }) => slug === this.slug)!;
@ -146,6 +129,10 @@ export class Org extends LiteElement {
return this.userOrg?.id || ""; return this.userOrg?.id || "";
} }
get org() {
return this.appState.org;
}
get isAdmin() { get isAdmin() {
const userOrg = this.userOrg; const userOrg = this.userOrg;
if (userOrg) return isAdmin(userOrg.role); if (userOrg) return isAdmin(userOrg.role);
@ -168,7 +155,6 @@ export class Org extends LiteElement {
"btrix-storage-quota-update", "btrix-storage-quota-update",
this.onStorageQuotaUpdate, this.onStorageQuotaUpdate,
); );
this.addEventListener("", () => {});
} }
disconnectedCallback() { disconnectedCallback() {
@ -202,6 +188,9 @@ export class Org extends LiteElement {
return; return;
} }
} else if (changedProperties.has("orgTab") && this.orgId) {
// Get most up to date org data
void this.updateOrg();
} }
if (changedProperties.has("openDialogName")) { if (changedProperties.has("openDialogName")) {
// Sync URL to create dialog // Sync URL to create dialog
@ -231,15 +220,11 @@ export class Org extends LiteElement {
if (!this.userInfo || !this.orgId) return; if (!this.userInfo || !this.orgId) return;
try { try {
this.org = await this.getOrg(this.orgId); const org = await this.getOrg(this.orgId);
this.showReadOnlyAlert = Boolean( AppStateService.updateOrg(org);
this.org?.readOnly || this.org?.subscription?.futureCancelDate, } catch (e) {
); console.debug(e);
this.checkStorageQuota();
this.checkExecutionMinutesQuota();
} catch {
this.notify({ this.notify({
message: msg("Sorry, couldn't retrieve organization at this time."), message: msg("Sorry, couldn't retrieve organization at this time."),
variant: "danger", variant: "danger",
@ -319,7 +304,7 @@ export class Org extends LiteElement {
return html` return html`
<div class="flex min-h-full flex-col"> <div class="flex min-h-full flex-col">
<btrix-org-status-banner .org=${this.org}></btrix-org-status-banner> <btrix-org-status-banner></btrix-org-status-banner>
${this.renderOrgNavBar()} ${this.renderOrgNavBar()}
<main <main
class="${noMaxWidth class="${noMaxWidth
@ -523,7 +508,7 @@ export class Org extends LiteElement {
.authState=${this.authState!} .authState=${this.authState!}
userId=${this.userInfo!.id} userId=${this.userInfo!.id}
orgId=${this.orgId} orgId=${this.orgId}
?orgStorageQuotaReached=${this.orgStorageQuotaReached} ?orgStorageQuotaReached=${this.org?.storageQuotaReached}
?isCrawler=${this.isCrawler} ?isCrawler=${this.isCrawler}
itemType=${ifDefined(params.itemType || undefined)} itemType=${ifDefined(params.itemType || undefined)}
@select-new-dialog=${this.onSelectNewDialog} @select-new-dialog=${this.onSelectNewDialog}
@ -543,9 +528,8 @@ export class Org extends LiteElement {
class="col-span-5 mt-6" class="col-span-5 mt-6"
.authState=${this.authState!} .authState=${this.authState!}
orgId=${this.orgId} orgId=${this.orgId}
?orgStorageQuotaReached=${this.orgStorageQuotaReached} ?orgStorageQuotaReached=${this.org?.storageQuotaReached}
?orgExecutionMinutesQuotaReached=${this ?orgExecutionMinutesQuotaReached=${this.org?.execMinutesQuotaReached}
.orgExecutionMinutesQuotaReached}
workflowId=${workflowId} workflowId=${workflowId}
openDialogName=${this.viewStateData?.dialog} openDialogName=${this.viewStateData?.dialog}
?isEditing=${isEditing} ?isEditing=${isEditing}
@ -566,8 +550,8 @@ export class Org extends LiteElement {
.initialWorkflow=${workflow} .initialWorkflow=${workflow}
.initialSeeds=${seeds} .initialSeeds=${seeds}
jobType=${ifDefined(params.jobType)} jobType=${ifDefined(params.jobType)}
?orgStorageQuotaReached=${this.orgStorageQuotaReached} ?orgStorageQuotaReached=${this.org?.storageQuotaReached}
?orgExecutionMinutesQuotaReached=${this.orgExecutionMinutesQuotaReached} ?orgExecutionMinutesQuotaReached=${this.org?.execMinutesQuotaReached}
@select-new-dialog=${this.onSelectNewDialog} @select-new-dialog=${this.onSelectNewDialog}
></btrix-workflows-new>`; ></btrix-workflows-new>`;
} }
@ -575,8 +559,8 @@ export class Org extends LiteElement {
return html`<btrix-workflows-list return html`<btrix-workflows-list
.authState=${this.authState!} .authState=${this.authState!}
orgId=${this.orgId} orgId=${this.orgId}
?orgStorageQuotaReached=${this.orgStorageQuotaReached} ?orgStorageQuotaReached=${this.org?.storageQuotaReached}
?orgExecutionMinutesQuotaReached=${this.orgExecutionMinutesQuotaReached} ?orgExecutionMinutesQuotaReached=${this.org?.execMinutesQuotaReached}
userId=${this.userInfo!.id} userId=${this.userInfo!.id}
?isCrawler=${this.isCrawler} ?isCrawler=${this.isCrawler}
@select-new-dialog=${this.onSelectNewDialog} @select-new-dialog=${this.onSelectNewDialog}
@ -686,22 +670,26 @@ export class Org extends LiteElement {
private async onStorageQuotaUpdate(e: CustomEvent<QuotaUpdateDetail>) { private async onStorageQuotaUpdate(e: CustomEvent<QuotaUpdateDetail>) {
e.stopPropagation(); e.stopPropagation();
const { reached } = e.detail; const { reached } = e.detail;
this.orgStorageQuotaReached = reached;
if (reached) { AppStateService.partialUpdateOrg({
this.showStorageQuotaAlert = true; id: this.orgId,
} storageQuotaReached: reached,
});
} }
private async onExecutionMinutesQuotaUpdate( private async onExecutionMinutesQuotaUpdate(
e: CustomEvent<QuotaUpdateDetail>, e: CustomEvent<QuotaUpdateDetail>,
) { ) {
e.stopPropagation(); e.stopPropagation();
const { reached } = e.detail; const { reached } = e.detail;
this.orgExecutionMinutesQuotaReached = reached;
if (reached) { AppStateService.partialUpdateOrg({
this.showExecutionMinutesQuotaAlert = true; id: this.orgId,
} execMinutesQuotaReached: reached,
});
} }
private async onUserRoleChange(e: UserRoleChangeEvent) { private async onUserRoleChange(e: UserRoleChangeEvent) {
@ -723,7 +711,9 @@ export class Org extends LiteElement {
variant: "success", variant: "success",
icon: "check2-circle", icon: "check2-circle",
}); });
this.org = await this.getOrg(this.orgId); const org = await this.getOrg(this.orgId);
AppStateService.updateOrg(org);
} catch (e) { } catch (e) {
console.debug(e); console.debug(e);
@ -742,14 +732,14 @@ export class Org extends LiteElement {
} }
private async removeMember(member: Member) { private async removeMember(member: Member) {
if (!this.org) return; const org = this.org;
if (!org) return;
const isSelf = member.email === this.userInfo!.email; const isSelf = member.email === this.userInfo!.email;
if ( if (
isSelf && isSelf &&
!window.confirm( !window.confirm(
msg( msg(str`Are you sure you want to remove yourself from ${org.name}?`),
str`Are you sure you want to remove yourself from ${this.org.name}?`,
),
) )
) { ) {
return; return;
@ -766,7 +756,7 @@ export class Org extends LiteElement {
this.notify({ this.notify({
message: msg( message: msg(
str`Successfully removed ${member.name || member.email} from ${ str`Successfully removed ${member.name || member.email} from ${
this.org.name org.name
}.`, }.`,
), ),
variant: "success", variant: "success",
@ -776,7 +766,9 @@ export class Org extends LiteElement {
// FIXME better UX, this is the only page currently that doesn't require org... // FIXME better UX, this is the only page currently that doesn't require org...
this.navTo("/account/settings"); this.navTo("/account/settings");
} else { } else {
this.org = await this.getOrg(this.orgId); const org = await this.getOrg(this.orgId);
AppStateService.updateOrg(org);
} }
} catch (e) { } catch (e) {
console.debug(e); console.debug(e);
@ -794,14 +786,4 @@ export class Org extends LiteElement {
}); });
} }
} }
checkStorageQuota() {
this.orgStorageQuotaReached = !!this.org?.storageQuotaReached;
this.showStorageQuotaAlert = this.orgStorageQuotaReached;
}
checkExecutionMinutesQuota() {
this.orgExecutionMinutesQuotaReached = !!this.org?.execMinutesQuotaReached;
this.showExecutionMinutesQuotaAlert = this.orgExecutionMinutesQuotaReached;
}
} }

View File

@ -6,6 +6,7 @@ import { locked, options, use } from "lit-shared-state";
import { persist } from "./persist"; import { persist } from "./persist";
import type { AppSettings } from "@/types/app"; import type { AppSettings } from "@/types/app";
import type { OrgData } from "@/types/org";
import type { CurrentUser } from "@/types/user"; import type { CurrentUser } from "@/types/user";
export { use }; export { use };
@ -20,6 +21,7 @@ type SlugLookup = Record<string, string>;
class AppState { class AppState {
settings: AppSettings | null = null; settings: AppSettings | null = null;
userInfo: CurrentUser | null = null; userInfo: CurrentUser | null = null;
org: OrgData | null | undefined = undefined;
@options(persist(window.localStorage)) @options(persist(window.localStorage))
orgSlug: string | null = null; orgSlug: string | null = null;
@ -57,6 +59,23 @@ export class AppStateService {
appState.userInfo = userInfo; appState.userInfo = userInfo;
}); });
}; };
static updateOrg = (org: AppState["org"]) => {
unlock(() => {
appState.org = org;
});
};
static partialUpdateOrg = (org: { id: string } & Partial<OrgData>) => {
unlock(() => {
if (org.id && appState.org?.id === org.id) {
appState.org = {
...appState.org,
...org,
};
} else {
console.warn("no matching org in app state");
}
});
};
static updateOrgSlug = (orgSlug: AppState["orgSlug"]) => { static updateOrgSlug = (orgSlug: AppState["orgSlug"]) => {
unlock(() => { unlock(() => {
appState.orgSlug = orgSlug; appState.orgSlug = orgSlug;