From 8d6375c654737138868343da5b11d674e0f7e65c Mon Sep 17 00:00:00 2001 From: sua yoo Date: Thu, 7 Dec 2023 11:42:20 -0800 Subject: [PATCH] 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 Co-authored-by: Emma Segal-Grossman --- frontend/src/controllers/api.ts | 22 ++++++------ frontend/src/pages/org/index.ts | 64 ++++++++++++++++++++++++++------- 2 files changed, 63 insertions(+), 23 deletions(-) diff --git a/frontend/src/controllers/api.ts b/frontend/src/controllers/api.ts index af811dba..487ac1ff 100644 --- a/frontend/src/controllers/api.ts +++ b/frontend/src/controllers/api.ts @@ -1,14 +1,12 @@ -import type { - LitElement, - ReactiveController, - ReactiveControllerHost, -} from "lit"; +import type { ReactiveController, ReactiveControllerHost } from "lit"; import { msg } from "@lit/localize"; import type { Auth } from "@/utils/AuthService"; import AuthService from "@/utils/AuthService"; import { APIError } from "@/utils/api"; +export type QuotaUpdate = { reached: boolean }; + /** * Utilities for interacting with the Browsertrix backend API * @@ -52,20 +50,22 @@ export class APIController implements ReactiveController { if (resp.ok) { const body = await resp.json(); const storageQuotaReached = body.storageQuotaReached; - const executionMinutesQuotaReached = body.executionMinutesQuotaReached; + const executionMinutesQuotaReached = body.execMinutesQuotaReached; if (typeof storageQuotaReached === "boolean") { this.host.dispatchEvent( - new CustomEvent("storage-quota-update", { + new CustomEvent("storage-quota-update", { detail: { reached: storageQuotaReached }, bubbles: true, + composed: true, }) ); } if (typeof executionMinutesQuotaReached === "boolean") { this.host.dispatchEvent( - new CustomEvent("execution-minutes-quota-update", { + new CustomEvent("execution-minutes-quota-update", { detail: { reached: executionMinutesQuotaReached }, bubbles: true, + composed: true, }) ); } @@ -89,9 +89,10 @@ export class APIController implements ReactiveController { case 403: { if (errorDetail === "storage_quota_reached") { this.host.dispatchEvent( - new CustomEvent("storage-quota-update", { + new CustomEvent("storage-quota-update", { detail: { reached: true }, bubbles: true, + composed: true, }) ); errorMessage = msg("Storage quota reached"); @@ -99,9 +100,10 @@ export class APIController implements ReactiveController { } if (errorDetail === "exec_minutes_quota_reached") { this.host.dispatchEvent( - new CustomEvent("execution-minutes-quota-update", { + new CustomEvent("execution-minutes-quota-update", { detail: { reached: true }, bubbles: true, + composed: true, }) ); errorMessage = msg("Monthly execution minutes quota reached"); diff --git a/frontend/src/pages/org/index.ts b/frontend/src/pages/org/index.ts index 4633db57..d9c2155f 100644 --- a/frontend/src/pages/org/index.ts +++ b/frontend/src/pages/org/index.ts @@ -33,6 +33,7 @@ import type { } from "./settings"; import type { Tab as CollectionTab } from "./collection-detail"; import type { SelectJobTypeEvent } from "@/features/crawl-workflows/new-workflow-dialog"; +import { type QuotaUpdate } from "@/controllers/api"; const RESOURCE_NAMES = ["workflow", "collection", "browser-profile", "upload"]; type ResourceName = (typeof RESOURCE_NAMES)[number]; @@ -58,6 +59,20 @@ type Params = { settingsTab?: "information" | "members"; new?: ResourceName; }; + +type OrgEventMap = { + "execution-minutes-quota-update": QuotaUpdate; + "storage-quota-update": QuotaUpdate; +}; + +type OrgEventListener = T extends keyof OrgEventMap + ? (this: Org, ev: CustomEvent) => 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 UUID_REGEX = @@ -134,6 +149,25 @@ export class Org extends LiteElement { 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) { if ( (changedProperties.has("userInfo") && this.userInfo) || @@ -469,7 +503,6 @@ export class Org extends LiteElement { workflowId=${this.params.workflowId || ""} itemType=${this.params.itemType || "crawl"} ?isCrawler=${this.isCrawler} - @storage-quota-update=${this.onStorageQuotaUpdate} >`; } @@ -480,7 +513,6 @@ export class Org extends LiteElement { ?orgStorageQuotaReached=${this.orgStorageQuotaReached} ?isCrawler=${this.isCrawler} itemType=${ifDefined(this.params.itemType || undefined)} - @storage-quota-update=${this.onStorageQuotaUpdate} @select-new-dialog=${this.onSelectNewDialog} >`; } @@ -504,8 +536,6 @@ export class Org extends LiteElement { openDialogName=${this.viewStateData?.dialog} ?isEditing=${isEditing} ?isCrawler=${this.isCrawler} - @storage-quota-update=${this.onStorageQuotaUpdate} - @execution-minutes-quota-update=${this.onExecutionMinutesQuotaUpdate} > `; } @@ -523,8 +553,6 @@ export class Org extends LiteElement { jobType=${ifDefined(this.params.jobType)} ?orgStorageQuotaReached=${this.orgStorageQuotaReached} ?orgExecutionMinutesQuotaReached=${this.orgExecutionMinutesQuotaReached} - @storage-quota-update=${this.onStorageQuotaUpdate} - @execution-minutes-quota-update=${this.onExecutionMinutesQuotaUpdate} @select-new-dialog=${this.onSelectNewDialog} >`; } @@ -536,8 +564,6 @@ export class Org extends LiteElement { ?orgExecutionMinutesQuotaReached=${this.orgExecutionMinutesQuotaReached} userId=${this.userInfo!.id} ?isCrawler=${this.isCrawler} - @storage-quota-update=${this.onStorageQuotaUpdate} - @execution-minutes-quota-update=${this.onExecutionMinutesQuotaUpdate} @select-new-dialog=${this.onSelectNewDialog} >`; } @@ -548,7 +574,6 @@ export class Org extends LiteElement { .authState=${this.authState!} .orgId=${this.orgId} profileId=${this.params.browserProfileId} - @storage-quota-update=${this.onStorageQuotaUpdate} >`; } @@ -557,14 +582,12 @@ export class Org extends LiteElement { .authState=${this.authState!} .orgId=${this.orgId} .browserId=${this.params.browserId} - @storage-quota-update=${this.onStorageQuotaUpdate} >`; } return html``; } @@ -671,7 +694,7 @@ export class Org extends LiteElement { this.removeMember(e.detail.member); } - private async onStorageQuotaUpdate(e: CustomEvent) { + private async onStorageQuotaUpdate(e: CustomEvent) { e.stopPropagation(); const { reached } = e.detail; this.orgStorageQuotaReached = reached; @@ -680,7 +703,7 @@ export class Org extends LiteElement { } } - private async onExecutionMinutesQuotaUpdate(e: CustomEvent) { + private async onExecutionMinutesQuotaUpdate(e: CustomEvent) { e.stopPropagation(); const { reached } = e.detail; this.orgExecutionMinutesQuotaReached = reached; @@ -789,4 +812,19 @@ export class Org extends LiteElement { this.orgExecutionMinutesQuotaReached = !!this.org?.execMinutesQuotaReached; this.showExecutionMinutesQuotaAlert = this.orgExecutionMinutesQuotaReached; } + + addEventListener( + type: T, + listener: OrgEventListener, + options?: boolean | AddEventListenerOptions + ): void { + super.addEventListener(type, listener as EventListener, options); + } + removeEventListener( + type: T, + listener: OrgEventListener, + options?: boolean | AddEventListenerOptions + ): void { + super.removeEventListener(type, listener as EventListener, options); + } }