feat: Show running crawl when editing workflow (#2481)

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 | <img width="354" alt="Screenshot 2025-03-11 at 1 34
07 PM"
src="https://github.com/user-attachments/assets/02f7fb4a-219d-43a4-bb1f-1f2b40ac1480"
/> |


<!-- ## Follow-ups -->

---------

Co-authored-by: emma <hi@emma.cafe>
This commit is contained in:
sua yoo 2025-03-18 15:54:04 -07:00 committed by GitHub
parent 89a6e84377
commit d2601a037e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 156 additions and 3 deletions

View File

@ -1,4 +1,5 @@
import("./exclusion-editor"); import("./exclusion-editor");
import("./live-workflow-status");
import("./new-workflow-dialog"); import("./new-workflow-dialog");
import("./queue-exclusion-form"); import("./queue-exclusion-form");
import("./queue-exclusion-table"); import("./queue-exclusion-table");

View File

@ -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<CrawlStatusChangedEventDetail>(
"btrix-crawl-status-changed",
{
detail: {
isCrawlRunning: workflow.isCrawlRunning,
state: workflow.lastCrawlState,
},
},
),
);
}
} else {
// dispatch status event on first run
this.dispatchEvent(
new CustomEvent<CrawlStatusChangedEventDetail>(
"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`
<btrix-crawl-status
class="block"
state=${lastCrawlState}
></btrix-crawl-status>
`;
});
}
private async getWorkflow(
workflowId: string,
signal: AbortSignal,
): Promise<Workflow> {
const data: Workflow = await this.api.fetch(
`/orgs/${this.orgId}/crawlconfigs/${workflowId}`,
{ signal },
);
return data;
}
}

View File

@ -49,6 +49,7 @@ import {
} from "@/controllers/observable"; } from "@/controllers/observable";
import { type SelectBrowserProfileChangeEvent } from "@/features/browser-profiles/select-browser-profile"; import { type SelectBrowserProfileChangeEvent } from "@/features/browser-profiles/select-browser-profile";
import type { CollectionsChangeEvent } from "@/features/collections/collections-add"; import type { CollectionsChangeEvent } from "@/features/collections/collections-add";
import type { CrawlStatusChangedEventDetail } from "@/features/crawl-workflows/live-workflow-status";
import type { import type {
ExclusionChangeEvent, ExclusionChangeEvent,
QueueExclusionTable, QueueExclusionTable,
@ -225,6 +226,9 @@ export class WorkflowEditor extends BtrixElement {
@state() @state()
private serverError?: TemplateResult | string; private serverError?: TemplateResult | string;
@state()
private isCrawlRunning: boolean | null = null;
// For observing panel sections position in viewport // For observing panel sections position in viewport
private readonly observable = new ObservableController(this, { private readonly observable = new ObservableController(this, {
// Add some padding to account for stickied elements // Add some padding to account for stickied elements
@ -580,6 +584,7 @@ export class WorkflowEditor extends BtrixElement {
` `
: nothing} : nothing}
${when(this.serverError, (error) => this.renderErrorAlert(error))} ${when(this.serverError, (error) => this.renderErrorAlert(error))}
${when(this.configId, this.renderCrawlStatus)}
<sl-tooltip content=${msg("Save without running")}> <sl-tooltip content=${msg("Save without running")}>
<sl-button <sl-button
@ -592,14 +597,21 @@ export class WorkflowEditor extends BtrixElement {
${msg("Save")} ${msg("Save")}
</sl-button> </sl-button>
</sl-tooltip> </sl-tooltip>
<sl-tooltip content=${msg("Save and run with new settings")}> <sl-tooltip
content=${this.isCrawlRunning
? msg("Crawl is already running")
: msg("Save and run with new settings")}
?disabled=${this.isCrawlRunning === null}
>
<sl-button <sl-button
size="small" size="small"
variant="primary" variant="primary"
type="submit" type="submit"
?disabled=${isArchivingDisabled(this.org, true) || ?disabled=${isArchivingDisabled(this.org, true) ||
this.isSubmitting} this.isSubmitting ||
?loading=${this.isSubmitting} this.isCrawlRunning ||
this.isCrawlRunning === null}
?loading=${this.isSubmitting || this.isCrawlRunning === null}
> >
${msg(html`Run Crawl`)} ${msg(html`Run Crawl`)}
</sl-button> </sl-button>
@ -608,6 +620,22 @@ export class WorkflowEditor extends BtrixElement {
`; `;
} }
private readonly renderCrawlStatus = (workflowId: string) => {
if (!workflowId) return;
return html`
<btrix-live-workflow-status
class="mx-2"
workflowId=${workflowId}
@btrix-crawl-status-changed=${(
e: CustomEvent<CrawlStatusChangedEventDetail>,
) => {
this.isCrawlRunning = e.detail.isCrawlRunning;
}}
></btrix-live-workflow-status>
`;
};
private renderSectionHeading(content: TemplateResult | string) { private renderSectionHeading(content: TemplateResult | string) {
return html` return html`
<btrix-section-heading class="col-span-5"> <btrix-section-heading class="col-span-5">