fix: Show latest crawl logs for failed workflows (#2694)

Shows "Logs" tab for failed workflows, and links directly to logs when
clicking a failed workflow in the workflow list.
This commit is contained in:
sua yoo 2025-06-30 10:12:06 -07:00 committed by GitHub
parent 5c78a57cbb
commit 0a68485c07
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 72 additions and 36 deletions

View File

@ -244,7 +244,7 @@ export class WorkflowListItem extends BtrixElement {
} }
e.preventDefault(); e.preventDefault();
await this.updateComplete; await this.updateComplete;
const href = `/orgs/${this.orgSlugState}/workflows/${this.workflow?.id}/${WorkflowTab.LatestCrawl}`; const href = `/orgs/${this.orgSlugState}/workflows/${this.workflow?.id}/${this.workflow?.lastCrawlState === "failed" ? WorkflowTab.Logs : WorkflowTab.LatestCrawl}`;
this.navigate.to(href); this.navigate.to(href);
}} }}
> >

View File

@ -28,12 +28,13 @@ import { pageNav, type Breadcrumb } from "@/layouts/pageHeader";
import { WorkflowTab } from "@/routes"; import { WorkflowTab } from "@/routes";
import { deleteConfirmation, noData, notApplicable } from "@/strings/ui"; import { deleteConfirmation, noData, notApplicable } from "@/strings/ui";
import type { APIPaginatedList, APIPaginationQuery } from "@/types/api"; import type { APIPaginatedList, APIPaginationQuery } from "@/types/api";
import { FAILED_STATES, type CrawlState } from "@/types/crawlState"; import { type CrawlState } from "@/types/crawlState";
import { isApiError } from "@/utils/api"; import { isApiError } from "@/utils/api";
import { import {
DEFAULT_MAX_SCALE, DEFAULT_MAX_SCALE,
inactiveCrawlStates, inactiveCrawlStates,
isActive, isActive,
isSkipped,
isSuccessfullyFinished, isSuccessfullyFinished,
} from "@/utils/crawler"; } from "@/utils/crawler";
import { humanizeSchedule } from "@/utils/cron"; import { humanizeSchedule } from "@/utils/cron";
@ -328,10 +329,12 @@ export class WorkflowDetail extends BtrixElement {
return this.workflow?.isCrawlRunning && !this.isPaused; return this.workflow?.isCrawlRunning && !this.isPaused;
} }
// Workflow is for a crawl that has failed or canceled private get isSkippedOrCanceled() {
private get isUnsuccessfullyFinished() { if (!this.workflow?.lastCrawlState) return null;
return (FAILED_STATES as readonly string[]).includes(
this.workflow?.lastCrawlState || "", return (
this.workflow.lastCrawlState === "canceled" ||
isSkipped({ state: this.workflow.lastCrawlState })
); );
} }
@ -678,6 +681,10 @@ export class WorkflowDetail extends BtrixElement {
const logTotals = this.logTotalsTask.value; const logTotals = this.logTotalsTask.value;
const authToken = this.authState?.headers.Authorization.split(" ")[1]; const authToken = this.authState?.headers.Authorization.split(" ")[1];
const disableDownload = this.isRunning; const disableDownload = this.isRunning;
const disableReplay = !latestCrawl.fileSize;
const disableLogs = !(logTotals?.errors || logTotals?.behaviors);
const replayHref = `/api/orgs/${this.orgId}/all-crawls/${latestCrawlId}/download?auth_bearer=${authToken}`;
const replayFilename = `browsertrix-${latestCrawlId}.wacz`;
return html` return html`
<btrix-copy-button <btrix-copy-button
@ -698,13 +705,13 @@ export class WorkflowDetail extends BtrixElement {
content="${msg("Download Item as WACZ")} (${this.localize.bytes( content="${msg("Download Item as WACZ")} (${this.localize.bytes(
latestCrawl.fileSize || 0, latestCrawl.fileSize || 0,
)})" )})"
?disabled=${!latestCrawl.fileSize} ?disabled=${disableReplay}
> >
<sl-button <sl-button
size="small" size="small"
href=${`/api/orgs/${this.orgId}/all-crawls/${latestCrawlId}/download?auth_bearer=${authToken}`} href=${replayHref}
download=${`browsertrix-${latestCrawlId}.wacz`} download=${replayFilename}
?disabled=${disableDownload || !latestCrawl.fileSize} ?disabled=${disableDownload || disableReplay}
> >
<sl-icon name="cloud-download" slot="prefix"></sl-icon> <sl-icon name="cloud-download" slot="prefix"></sl-icon>
${msg("Download")} ${msg("Download")}
@ -715,7 +722,7 @@ export class WorkflowDetail extends BtrixElement {
slot="trigger" slot="trigger"
size="small" size="small"
caret caret
?disabled=${disableDownload} ?disabled=${disableReplay && disableLogs}
> >
<sl-visually-hidden <sl-visually-hidden
>${msg("Download options")}</sl-visually-hidden >${msg("Download options")}</sl-visually-hidden
@ -723,9 +730,9 @@ export class WorkflowDetail extends BtrixElement {
</sl-button> </sl-button>
<sl-menu> <sl-menu>
<btrix-menu-item-link <btrix-menu-item-link
href=${`/api/orgs/${this.orgId}/all-crawls/${this.lastCrawlId}/download?auth_bearer=${authToken}`} href=${replayHref}
?disabled=${!latestCrawl.fileSize} ?disabled=${disableDownload || disableReplay}
download download=${replayFilename}
> >
<sl-icon name="cloud-download" slot="prefix"></sl-icon> <sl-icon name="cloud-download" slot="prefix"></sl-icon>
${msg("Item")} ${msg("Item")}
@ -741,7 +748,7 @@ export class WorkflowDetail extends BtrixElement {
</btrix-menu-item-link> </btrix-menu-item-link>
<btrix-menu-item-link <btrix-menu-item-link
href=${`/api/orgs/${this.orgId}/crawls/${this.lastCrawlId}/logs?auth_bearer=${authToken}`} href=${`/api/orgs/${this.orgId}/crawls/${this.lastCrawlId}/logs?auth_bearer=${authToken}`}
?disabled=${!(logTotals?.errors || logTotals?.behaviors)} ?disabled=${disableLogs}
download download
> >
<sl-icon <sl-icon
@ -1427,7 +1434,7 @@ export class WorkflowDetail extends BtrixElement {
}; };
private readonly renderLatestCrawl = () => { private readonly renderLatestCrawl = () => {
if (!this.lastCrawlId || this.isUnsuccessfullyFinished) { if (!this.lastCrawlId || this.isSkippedOrCanceled) {
return this.renderInactiveCrawlMessage(); return this.renderInactiveCrawlMessage();
} }
@ -1722,6 +1729,10 @@ export class WorkflowDetail extends BtrixElement {
</span>`; </span>`;
} }
if (!isSuccessfullyFinished({ state: workflow.lastCrawlState })) {
return notApplicable;
}
return html`<div class="inline-flex items-center gap-2"> return html`<div class="inline-flex items-center gap-2">
${latestCrawl.reviewStatus || !this.isCrawler ${latestCrawl.reviewStatus || !this.isCrawler
? html`<btrix-qa-review-status ? html`<btrix-qa-review-status
@ -1861,13 +1872,47 @@ export class WorkflowDetail extends BtrixElement {
let message = msg("This workflow hasnt been run yet."); let message = msg("This workflow hasnt been run yet.");
if (this.lastCrawlId) { if (this.lastCrawlId) {
if (this.workflow.lastCrawlState === "canceled") { switch (this.workflow.lastCrawlState) {
message = msg("This crawl cant be replayed since it was canceled."); case "canceled":
} else { message = msg("This crawl cant be replayed since it was canceled.");
message = msg("Replay is not available for this crawl."); break;
case "failed":
message = msg("This crawl cant be replayed because it failed.");
break;
default:
message = msg("Replay is not available for this crawl.");
break;
} }
} }
const actionButton = (workflow: Workflow) => {
if (!workflow.lastCrawlId) return;
if (workflow.lastCrawlState === "failed") {
return html`<div class="mt-4">
<sl-button
size="small"
href="${this.basePath}/logs"
@click=${this.navigate.link}
>
${msg("View Error Logs")}
<sl-icon slot="prefix" name="terminal-fill"></sl-icon>
</sl-button>
</div>`;
}
return html`<div class="mt-4">
<sl-button
size="small"
href="${this.basePath}/crawls/${workflow.lastCrawlId}"
@click=${this.navigate.link}
>
${msg("View Crawl Details")}
<sl-icon slot="suffix" name="arrow-right"></sl-icon>
</sl-button>
</div>`;
};
return html` return html`
<section <section
class="flex h-56 min-h-max flex-col items-center justify-center rounded-lg border p-4" class="flex h-56 min-h-max flex-col items-center justify-center rounded-lg border p-4"
@ -1878,20 +1923,7 @@ export class WorkflowDetail extends BtrixElement {
this.isCrawler && !this.lastCrawlId, this.isCrawler && !this.lastCrawlId,
() => html`<div class="mt-4">${this.renderRunNowButton()}</div>`, () => html`<div class="mt-4">${this.renderRunNowButton()}</div>`,
)} )}
${when( ${when(this.workflow, actionButton)}
this.lastCrawlId,
(id) =>
html`<div class="mt-4">
<sl-button
size="small"
href="${this.basePath}/crawls/${id}"
@click=${this.navigate.link}
>
${msg("View Crawl Details")}
<sl-icon slot="suffix" name="arrow-right"></sl-icon>
</sl-button>
</div>`,
)}
</section> </section>
`; `;
} }

View File

@ -33,11 +33,15 @@ export function isActive({ state }: Partial<Crawl | QARun>) {
return (activeCrawlStates as readonly (typeof state)[]).includes(state); return (activeCrawlStates as readonly (typeof state)[]).includes(state);
} }
export function isSuccessfullyFinished({ state }: { state: string }) { export function isSuccessfullyFinished({ state }: { state: string | null }) {
return state && (SUCCESSFUL_STATES as readonly string[]).includes(state); return state && (SUCCESSFUL_STATES as readonly string[]).includes(state);
} }
export function isNotFailed({ state }: { state: string }) { export function isSkipped({ state }: { state: string | null }) {
return state?.startsWith("skipped");
}
export function isNotFailed({ state }: { state: string | null }) {
return ( return (
state && !(FAILED_STATES as readonly string[]).some((str) => str === state) state && !(FAILED_STATES as readonly string[]).some((str) => str === state)
); );