From d2601a037e772c4815fc3d0ffcd484989001da8f Mon Sep 17 00:00:00 2001 From: sua yoo Date: Tue, 18 Mar 2025 15:54:04 -0700 Subject: [PATCH] feat: Show running crawl when editing workflow (#2481) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Part of https://github.com/webrecorder/browsertrix/issues/2366 ## Changes - Displays latest running crawl status when editing workflow - Disables "Run Now" button if crawl is currently running Currently, clicking "Run Now" will result in a preventable server error if the crawl is already running. The change in this PR is in preparation for being able to update a currently running crawl and doesn't require any backend changes. ## Manual testing 1. Log in as crawler 2. Go to edit crawl workflow 3. Open same workflow in another tab 4. Run the workflow 5. Go back to edit tab. Verify "Starting" status is shown next to "Save" button and "Run Crawl" button is disabled ## Screenshots | Page | Image/video | | ---- | ----------- | | Edit Workflow | Screenshot 2025-03-11 at 1 34
07 PM | --------- Co-authored-by: emma --- .../src/features/crawl-workflows/index.ts | 1 + .../crawl-workflows/live-workflow-status.ts | 124 ++++++++++++++++++ .../crawl-workflows/workflow-editor.ts | 34 ++++- 3 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 frontend/src/features/crawl-workflows/live-workflow-status.ts diff --git a/frontend/src/features/crawl-workflows/index.ts b/frontend/src/features/crawl-workflows/index.ts index 46ee28ae..77af83b0 100644 --- a/frontend/src/features/crawl-workflows/index.ts +++ b/frontend/src/features/crawl-workflows/index.ts @@ -1,4 +1,5 @@ import("./exclusion-editor"); +import("./live-workflow-status"); import("./new-workflow-dialog"); import("./queue-exclusion-form"); import("./queue-exclusion-table"); diff --git a/frontend/src/features/crawl-workflows/live-workflow-status.ts b/frontend/src/features/crawl-workflows/live-workflow-status.ts new file mode 100644 index 00000000..a4e11e2e --- /dev/null +++ b/frontend/src/features/crawl-workflows/live-workflow-status.ts @@ -0,0 +1,124 @@ +import { localized } from "@lit/localize"; +import { Task } from "@lit/task"; +import { html } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import { guard } from "lit/directives/guard.js"; + +import { BtrixElement } from "@/classes/BtrixElement"; +import type { Workflow } from "@/types/crawler"; + +export type CrawlStatusChangedEventDetail = { + isCrawlRunning: Workflow["isCrawlRunning"]; + state: Workflow["lastCrawlState"]; +}; + +const POLL_INTERVAL_SECONDS = 5; + +/** + * Current workflow status, displayed "live" by polling + * + * @fires btrix-crawl-status-changed + */ +@customElement("btrix-live-workflow-status") +@localized() +export class LiveWorkflowStatus extends BtrixElement { + @property({ type: String }) + workflowId = ""; + + private readonly workflowTask = new Task(this, { + task: async ([workflowId], { signal }) => { + if (!workflowId) throw new Error("required `workflowId` missing"); + + try { + const workflow = await this.getWorkflow(workflowId, signal); + + if (this.workflowTask.value) { + if ( + this.workflowTask.value.lastCrawlState !== workflow.lastCrawlState + ) { + this.dispatchEvent( + new CustomEvent( + "btrix-crawl-status-changed", + { + detail: { + isCrawlRunning: workflow.isCrawlRunning, + state: workflow.lastCrawlState, + }, + }, + ), + ); + } + } else { + // dispatch status event on first run + this.dispatchEvent( + new CustomEvent( + "btrix-crawl-status-changed", + { + detail: { + isCrawlRunning: workflow.isCrawlRunning, + state: workflow.lastCrawlState, + }, + }, + ), + ); + } + + return workflow; + } catch (e) { + if ((e as Error).name === "AbortError") { + console.debug("Fetch archived items aborted to throttle"); + } else { + console.debug(e); + } + throw e; + } + }, + args: () => [this.workflowId] as const, + }); + + private readonly pollTask = new Task(this, { + task: async ([workflow]) => { + if (!workflow) return; + + return window.setTimeout(() => { + void this.workflowTask.run(); + }, POLL_INTERVAL_SECONDS * 1000); + }, + args: () => [this.workflowTask.value] as const, + }); + + disconnectedCallback(): void { + super.disconnectedCallback(); + + if (this.pollTask.value) { + window.clearTimeout(this.pollTask.value); + } + } + + render() { + const workflow = this.workflowTask.value; + const lastCrawlState = workflow?.lastCrawlState; + + if (!workflow?.isCrawlRunning || !lastCrawlState) return; + + return guard([lastCrawlState], () => { + return html` + + `; + }); + } + + private async getWorkflow( + workflowId: string, + signal: AbortSignal, + ): Promise { + const data: Workflow = await this.api.fetch( + `/orgs/${this.orgId}/crawlconfigs/${workflowId}`, + { signal }, + ); + return data; + } +} diff --git a/frontend/src/features/crawl-workflows/workflow-editor.ts b/frontend/src/features/crawl-workflows/workflow-editor.ts index 920e2517..765fbad4 100644 --- a/frontend/src/features/crawl-workflows/workflow-editor.ts +++ b/frontend/src/features/crawl-workflows/workflow-editor.ts @@ -49,6 +49,7 @@ import { } from "@/controllers/observable"; import { type SelectBrowserProfileChangeEvent } from "@/features/browser-profiles/select-browser-profile"; import type { CollectionsChangeEvent } from "@/features/collections/collections-add"; +import type { CrawlStatusChangedEventDetail } from "@/features/crawl-workflows/live-workflow-status"; import type { ExclusionChangeEvent, QueueExclusionTable, @@ -225,6 +226,9 @@ export class WorkflowEditor extends BtrixElement { @state() private serverError?: TemplateResult | string; + @state() + private isCrawlRunning: boolean | null = null; + // For observing panel sections position in viewport private readonly observable = new ObservableController(this, { // Add some padding to account for stickied elements @@ -580,6 +584,7 @@ export class WorkflowEditor extends BtrixElement { ` : nothing} ${when(this.serverError, (error) => this.renderErrorAlert(error))} + ${when(this.configId, this.renderCrawlStatus)} - + ${msg(html`Run Crawl`)} @@ -608,6 +620,22 @@ export class WorkflowEditor extends BtrixElement { `; } + private readonly renderCrawlStatus = (workflowId: string) => { + if (!workflowId) return; + + return html` + , + ) => { + this.isCrawlRunning = e.detail.isCrawlRunning; + }} + > + `; + }; + private renderSectionHeading(content: TemplateResult | string) { return html`