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`