Fix UI not updating after quotas reached status changes (#1425)

Fixes #1426 

- Update expected org field for execMinutesQuotaReached
- Move event handlers from child components to org index connectedCallback
- Add composed to events
- Improve typing



Co-authored-by: Tessa Walsh <tessa@bitarchivist.net>
Co-authored-by: Emma Segal-Grossman <hi@emma.cafe>
This commit is contained in:
sua yoo 2023-12-07 11:42:20 -08:00 committed by GitHub
parent be41c48c27
commit 8d6375c654
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 63 additions and 23 deletions

View File

@ -1,14 +1,12 @@
import type { import type { ReactiveController, ReactiveControllerHost } from "lit";
LitElement,
ReactiveController,
ReactiveControllerHost,
} from "lit";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import type { Auth } from "@/utils/AuthService"; import type { Auth } from "@/utils/AuthService";
import AuthService from "@/utils/AuthService"; import AuthService from "@/utils/AuthService";
import { APIError } from "@/utils/api"; import { APIError } from "@/utils/api";
export type QuotaUpdate = { reached: boolean };
/** /**
* Utilities for interacting with the Browsertrix backend API * Utilities for interacting with the Browsertrix backend API
* *
@ -52,20 +50,22 @@ export class APIController implements ReactiveController {
if (resp.ok) { if (resp.ok) {
const body = await resp.json(); const body = await resp.json();
const storageQuotaReached = body.storageQuotaReached; const storageQuotaReached = body.storageQuotaReached;
const executionMinutesQuotaReached = body.executionMinutesQuotaReached; const executionMinutesQuotaReached = body.execMinutesQuotaReached;
if (typeof storageQuotaReached === "boolean") { if (typeof storageQuotaReached === "boolean") {
this.host.dispatchEvent( this.host.dispatchEvent(
new CustomEvent("storage-quota-update", { new CustomEvent<QuotaUpdate>("storage-quota-update", {
detail: { reached: storageQuotaReached }, detail: { reached: storageQuotaReached },
bubbles: true, bubbles: true,
composed: true,
}) })
); );
} }
if (typeof executionMinutesQuotaReached === "boolean") { if (typeof executionMinutesQuotaReached === "boolean") {
this.host.dispatchEvent( this.host.dispatchEvent(
new CustomEvent("execution-minutes-quota-update", { new CustomEvent<QuotaUpdate>("execution-minutes-quota-update", {
detail: { reached: executionMinutesQuotaReached }, detail: { reached: executionMinutesQuotaReached },
bubbles: true, bubbles: true,
composed: true,
}) })
); );
} }
@ -89,9 +89,10 @@ export class APIController implements ReactiveController {
case 403: { case 403: {
if (errorDetail === "storage_quota_reached") { if (errorDetail === "storage_quota_reached") {
this.host.dispatchEvent( this.host.dispatchEvent(
new CustomEvent("storage-quota-update", { new CustomEvent<QuotaUpdate>("storage-quota-update", {
detail: { reached: true }, detail: { reached: true },
bubbles: true, bubbles: true,
composed: true,
}) })
); );
errorMessage = msg("Storage quota reached"); errorMessage = msg("Storage quota reached");
@ -99,9 +100,10 @@ export class APIController implements ReactiveController {
} }
if (errorDetail === "exec_minutes_quota_reached") { if (errorDetail === "exec_minutes_quota_reached") {
this.host.dispatchEvent( this.host.dispatchEvent(
new CustomEvent("execution-minutes-quota-update", { new CustomEvent<QuotaUpdate>("execution-minutes-quota-update", {
detail: { reached: true }, detail: { reached: true },
bubbles: true, bubbles: true,
composed: true,
}) })
); );
errorMessage = msg("Monthly execution minutes quota reached"); errorMessage = msg("Monthly execution minutes quota reached");

View File

@ -33,6 +33,7 @@ import type {
} from "./settings"; } from "./settings";
import type { Tab as CollectionTab } from "./collection-detail"; import type { Tab as CollectionTab } from "./collection-detail";
import type { SelectJobTypeEvent } from "@/features/crawl-workflows/new-workflow-dialog"; import type { SelectJobTypeEvent } from "@/features/crawl-workflows/new-workflow-dialog";
import { type QuotaUpdate } from "@/controllers/api";
const RESOURCE_NAMES = ["workflow", "collection", "browser-profile", "upload"]; const RESOURCE_NAMES = ["workflow", "collection", "browser-profile", "upload"];
type ResourceName = (typeof RESOURCE_NAMES)[number]; type ResourceName = (typeof RESOURCE_NAMES)[number];
@ -58,6 +59,20 @@ type Params = {
settingsTab?: "information" | "members"; settingsTab?: "information" | "members";
new?: ResourceName; new?: ResourceName;
}; };
type OrgEventMap = {
"execution-minutes-quota-update": QuotaUpdate;
"storage-quota-update": QuotaUpdate;
};
type OrgEventListener<T extends string> = T extends keyof OrgEventMap
? (this: Org, ev: CustomEvent<OrgEventMap[T]>) => unknown
: EventListenerOrEventListenerObject;
// `string & {}` is resolved to `string`, but not by intellisense, so this gives us string suggestions in vscode from OrgEventMap but still allows arbitrary strings
// eslint-disable-next-line @typescript-eslint/ban-types
type EventType = keyof OrgEventMap | (string & {});
const defaultTab = "home"; const defaultTab = "home";
const UUID_REGEX = const UUID_REGEX =
@ -134,6 +149,25 @@ export class Org extends LiteElement {
return false; return false;
} }
connectedCallback() {
super.connectedCallback();
this.addEventListener(
"execution-minutes-quota-update",
this.onExecutionMinutesQuotaUpdate
);
this.addEventListener("storage-quota-update", this.onStorageQuotaUpdate);
this.addEventListener("", () => {});
}
disconnectedCallback() {
this.removeEventListener(
"execution-minutes-quota-update",
this.onExecutionMinutesQuotaUpdate
);
this.removeEventListener("storage-quota-update", this.onStorageQuotaUpdate);
this.disconnectedCallback();
}
async willUpdate(changedProperties: Map<string, any>) { async willUpdate(changedProperties: Map<string, any>) {
if ( if (
(changedProperties.has("userInfo") && this.userInfo) || (changedProperties.has("userInfo") && this.userInfo) ||
@ -469,7 +503,6 @@ export class Org extends LiteElement {
workflowId=${this.params.workflowId || ""} workflowId=${this.params.workflowId || ""}
itemType=${this.params.itemType || "crawl"} itemType=${this.params.itemType || "crawl"}
?isCrawler=${this.isCrawler} ?isCrawler=${this.isCrawler}
@storage-quota-update=${this.onStorageQuotaUpdate}
></btrix-crawl-detail>`; ></btrix-crawl-detail>`;
} }
@ -480,7 +513,6 @@ export class Org extends LiteElement {
?orgStorageQuotaReached=${this.orgStorageQuotaReached} ?orgStorageQuotaReached=${this.orgStorageQuotaReached}
?isCrawler=${this.isCrawler} ?isCrawler=${this.isCrawler}
itemType=${ifDefined(this.params.itemType || undefined)} itemType=${ifDefined(this.params.itemType || undefined)}
@storage-quota-update=${this.onStorageQuotaUpdate}
@select-new-dialog=${this.onSelectNewDialog} @select-new-dialog=${this.onSelectNewDialog}
></btrix-crawls-list>`; ></btrix-crawls-list>`;
} }
@ -504,8 +536,6 @@ export class Org extends LiteElement {
openDialogName=${this.viewStateData?.dialog} openDialogName=${this.viewStateData?.dialog}
?isEditing=${isEditing} ?isEditing=${isEditing}
?isCrawler=${this.isCrawler} ?isCrawler=${this.isCrawler}
@storage-quota-update=${this.onStorageQuotaUpdate}
@execution-minutes-quota-update=${this.onExecutionMinutesQuotaUpdate}
></btrix-workflow-detail> ></btrix-workflow-detail>
`; `;
} }
@ -523,8 +553,6 @@ export class Org extends LiteElement {
jobType=${ifDefined(this.params.jobType)} jobType=${ifDefined(this.params.jobType)}
?orgStorageQuotaReached=${this.orgStorageQuotaReached} ?orgStorageQuotaReached=${this.orgStorageQuotaReached}
?orgExecutionMinutesQuotaReached=${this.orgExecutionMinutesQuotaReached} ?orgExecutionMinutesQuotaReached=${this.orgExecutionMinutesQuotaReached}
@storage-quota-update=${this.onStorageQuotaUpdate}
@execution-minutes-quota-update=${this.onExecutionMinutesQuotaUpdate}
@select-new-dialog=${this.onSelectNewDialog} @select-new-dialog=${this.onSelectNewDialog}
></btrix-workflows-new>`; ></btrix-workflows-new>`;
} }
@ -536,8 +564,6 @@ export class Org extends LiteElement {
?orgExecutionMinutesQuotaReached=${this.orgExecutionMinutesQuotaReached} ?orgExecutionMinutesQuotaReached=${this.orgExecutionMinutesQuotaReached}
userId=${this.userInfo!.id} userId=${this.userInfo!.id}
?isCrawler=${this.isCrawler} ?isCrawler=${this.isCrawler}
@storage-quota-update=${this.onStorageQuotaUpdate}
@execution-minutes-quota-update=${this.onExecutionMinutesQuotaUpdate}
@select-new-dialog=${this.onSelectNewDialog} @select-new-dialog=${this.onSelectNewDialog}
></btrix-workflows-list>`; ></btrix-workflows-list>`;
} }
@ -548,7 +574,6 @@ export class Org extends LiteElement {
.authState=${this.authState!} .authState=${this.authState!}
.orgId=${this.orgId} .orgId=${this.orgId}
profileId=${this.params.browserProfileId} profileId=${this.params.browserProfileId}
@storage-quota-update=${this.onStorageQuotaUpdate}
></btrix-browser-profiles-detail>`; ></btrix-browser-profiles-detail>`;
} }
@ -557,14 +582,12 @@ export class Org extends LiteElement {
.authState=${this.authState!} .authState=${this.authState!}
.orgId=${this.orgId} .orgId=${this.orgId}
.browserId=${this.params.browserId} .browserId=${this.params.browserId}
@storage-quota-update=${this.onStorageQuotaUpdate}
></btrix-browser-profiles-new>`; ></btrix-browser-profiles-new>`;
} }
return html`<btrix-browser-profiles-list return html`<btrix-browser-profiles-list
.authState=${this.authState!} .authState=${this.authState!}
.orgId=${this.orgId} .orgId=${this.orgId}
@storage-quota-update=${this.onStorageQuotaUpdate}
@select-new-dialog=${this.onSelectNewDialog} @select-new-dialog=${this.onSelectNewDialog}
></btrix-browser-profiles-list>`; ></btrix-browser-profiles-list>`;
} }
@ -671,7 +694,7 @@ export class Org extends LiteElement {
this.removeMember(e.detail.member); this.removeMember(e.detail.member);
} }
private async onStorageQuotaUpdate(e: CustomEvent) { private async onStorageQuotaUpdate(e: CustomEvent<QuotaUpdate>) {
e.stopPropagation(); e.stopPropagation();
const { reached } = e.detail; const { reached } = e.detail;
this.orgStorageQuotaReached = reached; this.orgStorageQuotaReached = reached;
@ -680,7 +703,7 @@ export class Org extends LiteElement {
} }
} }
private async onExecutionMinutesQuotaUpdate(e: CustomEvent) { private async onExecutionMinutesQuotaUpdate(e: CustomEvent<QuotaUpdate>) {
e.stopPropagation(); e.stopPropagation();
const { reached } = e.detail; const { reached } = e.detail;
this.orgExecutionMinutesQuotaReached = reached; this.orgExecutionMinutesQuotaReached = reached;
@ -789,4 +812,19 @@ export class Org extends LiteElement {
this.orgExecutionMinutesQuotaReached = !!this.org?.execMinutesQuotaReached; this.orgExecutionMinutesQuotaReached = !!this.org?.execMinutesQuotaReached;
this.showExecutionMinutesQuotaAlert = this.orgExecutionMinutesQuotaReached; this.showExecutionMinutesQuotaAlert = this.orgExecutionMinutesQuotaReached;
} }
addEventListener<T extends EventType>(
type: T,
listener: OrgEventListener<T>,
options?: boolean | AddEventListenerOptions
): void {
super.addEventListener(type, listener as EventListener, options);
}
removeEventListener<T extends EventType>(
type: T,
listener: OrgEventListener<T>,
options?: boolean | AddEventListenerOptions
): void {
super.removeEventListener(type, listener as EventListener, options);
}
} }