@@ -984,25 +1477,125 @@ export class WorkflowDetail extends BtrixElement {
name=${WorkflowTab.LatestCrawl}
class="mt-3 block"
>
- ${when(this.workflow?.isCrawlRunning, this.renderWatchCrawl, () =>
- this.renderInactiveWatchCrawl(),
+
+ ${when(this.workflowTab === WorkflowTab.LatestCrawl, () =>
+ when(
+ showReplay,
+ this.renderInactiveWatchCrawl,
+ this.renderWatchCrawl,
+ ),
)}
`;
};
+ private readonly renderPausedNotice = (
+ { truncate } = { truncate: false },
+ ) => {
+ if (
+ !this.workflow ||
+ !this.isPaused ||
+ this.isResuming ||
+ !this.workflow.lastCrawlPausedExpiry
+ )
+ return;
+
+ const diff =
+ new Date(this.workflow.lastCrawlPausedExpiry).valueOf() -
+ new Date().valueOf();
+
+ if (diff < 0) return;
+
+ const formattedDate = this.localize.date(
+ this.workflow.lastCrawlPausedExpiry,
+ );
+
+ const infoIcon = html`
+ `;
+ };
+
private renderLatestCrawlAction() {
- if (this.isCrawler && this.workflow?.isCrawlRunning) {
+ if (!this.workflow || !this.lastCrawlId) return;
+
+ if (this.isRunning) {
+ if (!this.isCrawler) return;
+
const enableEditBrowserWindows = !this.workflow.lastCrawlStopping;
const windowCount =
this.workflow.scale * (this.appState.settings?.numBrowsers || 1);
return html`
-
+
${msg("Running in")} ${this.localize.number(windowCount)}
${pluralOf("browserWindows", windowCount)}
@@ -1024,93 +1617,118 @@ export class WorkflowDetail extends BtrixElement {
`;
}
-
- const authToken = this.authState?.headers.Authorization.split(" ")[1];
-
- if (
- this.workflowTab === WorkflowTab.LatestCrawl &&
- this.lastCrawlId &&
- this.workflow?.lastCrawlSize
- ) {
- return html`
-
-
- `;
- }
-
- if (
- this.workflowTab === WorkflowTab.Logs &&
- (this.logTotals?.errors || this.logTotals?.behaviors)
- ) {
- return html`
-
-
- `;
- }
}
private readonly renderCrawlDetails = () => {
+ const latestCrawl = this.latestCrawlTask.value;
const skeleton = html`
`;
+ const duration = (workflow: Workflow) => {
+ if (!workflow.lastCrawlStartTime) return skeleton;
+
+ return this.localize.humanizeDuration(
+ (workflow.lastCrawlTime && !workflow.isCrawlRunning
+ ? new Date(workflow.lastCrawlTime)
+ : new Date()
+ ).valueOf() - new Date(workflow.lastCrawlStartTime).valueOf(),
+ );
+ };
+
+ const execTime = () => {
+ if (!latestCrawl) return skeleton;
+
+ if (this.isRunning) {
+ return html`
+ ${noData}
+
+
+
+ `;
+ }
+
+ if (latestCrawl.crawlExecSeconds < 60) {
+ return this.localize.humanizeDuration(
+ latestCrawl.crawlExecSeconds * 1000,
+ );
+ }
+
+ return humanizeExecutionSeconds(latestCrawl.crawlExecSeconds, {
+ style: "short",
+ });
+ };
+
const pages = (workflow: Workflow) => {
- if (!this.lastCrawl) return skeleton;
+ if (!latestCrawl) return skeleton;
if (workflow.isCrawlRunning) {
return [
- this.localize.number(+(this.lastCrawl.stats?.done || 0)),
- this.localize.number(+(this.lastCrawl.stats?.found || 0)),
- ].join(" / ");
+ this.localize.number(+(latestCrawl.stats?.done || 0)),
+ this.localize.number(+(latestCrawl.stats?.found || 0)),
+ ].join(` ${msg("of")} `);
}
- return this.localize.number(this.lastCrawl.pageCount || 0);
+ return this.localize.number(latestCrawl.pageCount || 0);
};
const qa = (workflow: Workflow) => {
- if (!this.lastCrawl)
- return html`
`;
+ if (!latestCrawl) return html`
`;
if (workflow.isCrawlRunning) {
return html`
${noData}
-
-
+
`;
}
- return html`
`;
+ return html`
+ ${latestCrawl.reviewStatus || !this.isCrawler
+ ? html``
+ : html`
+
+ ${msg("Add Review")}
+ `}
+
`;
};
return html`
${this.renderDetailItem(msg("Run Duration"), (workflow) =>
- this.lastCrawlStartTime
- ? this.localize.humanizeDuration(
- (workflow.lastCrawlTime && !workflow.isCrawlRunning
- ? new Date(workflow.lastCrawlTime)
- : new Date()
- ).valueOf() - new Date(this.lastCrawlStartTime).valueOf(),
- )
- : skeleton,
+ isLoading(this.runNowTask)
+ ? html`${until(
+ this.runNowTask.taskComplete.then((workflow) =>
+ workflow ? duration(workflow) : noData,
+ ),
+ html``,
+ )}`
+ : duration(workflow),
+ )}
+ ${this.renderDetailItem(msg("Execution Time"), () =>
+ isLoading(this.runNowTask)
+ ? html`${until(
+ this.runNowTask.taskComplete.then((workflow) =>
+ workflow ? execTime() : noData,
+ ),
+ html``,
+ )}`
+ : execTime(),
)}
${this.renderDetailItem(msg("Pages Crawled"), pages)}
${this.renderDetailItem(msg("Size"), (workflow) =>
@@ -1129,7 +1747,7 @@ export class WorkflowDetail extends BtrixElement {
// Show custom message if crawl is active but not explicitly running
let waitingMsg: string | null = null;
- if (!this.isExplicitRunning) {
+ if (!this.isCrawling) {
switch (this.workflow.lastCrawlState) {
case "starting":
waitingMsg = msg("Crawl starting...");
@@ -1165,7 +1783,7 @@ export class WorkflowDetail extends BtrixElement {
return html`
${when(
- this.isExplicitRunning && this.workflow,
+ this.isCrawling && this.workflow,
(workflow) => html`
{
if (!this.workflow) return;
if (!this.lastCrawlId || !this.workflow.lastCrawlSize) {
@@ -1198,10 +1816,10 @@ export class WorkflowDetail extends BtrixElement {
return html`
- ${this.renderReplay()}
+ ${guard([this.lastCrawlId], this.renderReplay)}
`;
- }
+ };
private renderInactiveCrawlMessage() {
if (!this.workflow) return;
@@ -1228,11 +1846,11 @@ export class WorkflowDetail extends BtrixElement {
)}
${when(
this.lastCrawlId,
- () =>
+ (id) =>
html`
${msg("View Crawl Details")}
@@ -1244,7 +1862,7 @@ export class WorkflowDetail extends BtrixElement {
`;
}
- private renderReplay() {
+ private readonly renderReplay = () => {
if (!this.workflow || !this.lastCrawlId) return;
const replaySource = `/api/orgs/${this.workflow.oid}/crawls/${this.lastCrawlId}/replay.json`;
@@ -1262,27 +1880,27 @@ export class WorkflowDetail extends BtrixElement {
noCache="true"
>
`;
- }
+ };
- private renderLogs() {
+ private readonly renderLogs = () => {
return html`
-
+
${when(
this.lastCrawlId,
(crawlId) => html`
`,
() => this.renderNoCrawlLogs(),
)}
`;
- }
+ };
private readonly renderRunNowButton = () => {
return html`
@@ -1295,10 +1913,12 @@ export class WorkflowDetail extends BtrixElement {
size="small"
variant="primary"
?disabled=${this.org?.storageQuotaReached ||
- this.org?.execMinutesQuotaReached}
- @click=${() => void this.runNow()}
+ this.org?.execMinutesQuotaReached ||
+ isLoading(this.runNowTask)}
+ ?loading=${isLoading(this.runNowTask)}
+ @click=${() => void this.runNowTask.run()}
>
-
+
${msg("Run Crawl")}
@@ -1404,10 +2024,10 @@ export class WorkflowDetail extends BtrixElement {
value=${value}
size="small"
@click=${async () => {
- await this.scale(value);
+ await this.scaleTask.run([value]);
this.openDialogName = undefined;
}}
- ?disabled=${this.isSubmittingUpdate}
+ ?disabled=${isLoading(this.scaleTask)}
>${label}
`,
@@ -1418,7 +2038,10 @@ export class WorkflowDetail extends BtrixElement {
(this.openDialogName = undefined)}
+ @click=${() => {
+ this.scaleTask.abort();
+ this.openDialogName = undefined;
+ }}
>${msg("Cancel")}
@@ -1429,7 +2052,7 @@ export class WorkflowDetail extends BtrixElement {
return html`
`;
private readonly showDialog = async () => {
- await this.getWorkflowPromise;
+ await this.workflowTask.taskComplete;
this.isDialogVisible = true;
};
private handleExclusionChange() {
- void this.fetchWorkflow();
+ void this.workflowTask.run();
}
- private async scale(value: Crawl["scale"]) {
+ private async scale(value: Crawl["scale"], signal: AbortSignal) {
if (!this.lastCrawlId) return;
- this.isSubmittingUpdate = true;
try {
const data = await this.api.fetch<{ scaled: boolean }>(
@@ -1463,11 +2085,11 @@ export class WorkflowDetail extends BtrixElement {
{
method: "POST",
body: JSON.stringify({ scale: +value }),
+ signal,
},
);
if (data.scaled) {
- void this.fetchWorkflow();
this.notify.toast({
message: msg("Updated number of browser windows."),
variant: "success",
@@ -1487,13 +2109,12 @@ export class WorkflowDetail extends BtrixElement {
id: "browser-windows-update-status",
});
}
-
- this.isSubmittingUpdate = false;
}
- private async getWorkflow(): Promise {
- const data: Workflow = await this.api.fetch(
- `/orgs/${this.orgId}/crawlconfigs/${this.workflowId}`,
+ private async getWorkflow(workflowId: string, signal: AbortSignal) {
+ const data = await this.api.fetch(
+ `/orgs/${this.orgId}/crawlconfigs/${workflowId}`,
+ { signal },
);
return data;
}
@@ -1506,118 +2127,68 @@ export class WorkflowDetail extends BtrixElement {
this.openDialogName = undefined;
}
- private async fetchSeeds(): Promise {
- try {
- this.getSeedsPromise = this.getSeeds();
- this.seeds = await this.getSeedsPromise;
- } catch {
- this.notify.toast({
- message: msg(
- "Sorry, couldn't retrieve all crawl settings at this time.",
- ),
- variant: "danger",
- icon: "exclamation-octagon",
- id: "data-retrieve-error",
- });
- }
- }
-
- private async getSeeds() {
+ private async getSeeds(workflowId: string, signal: AbortSignal) {
const data = await this.api.fetch>(
- `/orgs/${this.orgId}/crawlconfigs/${this.workflowId}/seeds`,
+ `/orgs/${this.orgId}/crawlconfigs/${workflowId}/seeds`,
+ { signal },
);
return data;
}
- private async fetchCrawls() {
- try {
- this.crawls = await this.getCrawls();
- } catch {
- this.notify.toast({
- message: msg("Sorry, couldn't get crawls at this time."),
- variant: "danger",
- icon: "exclamation-octagon",
- id: "data-retrieve-error",
- });
- }
- }
-
- private async getCrawls() {
+ private async getCrawls(
+ workflowId: string,
+ params: WorkflowDetail["crawlsParams"],
+ signal: AbortSignal,
+ ) {
const query = queryString.stringify(
{
- state: this.filterBy.state,
- cid: this.workflowId,
+ cid: workflowId,
sortBy: "started",
+ page: params.page ?? this.crawls?.page,
+ pageSize: this.crawls?.pageSize ?? 10,
+ ...params,
},
{
arrayFormat: "comma",
},
);
+
const data = await this.api.fetch>(
`/orgs/${this.orgId}/crawls?${query}`,
+ { signal },
);
return data;
}
private stopPoll() {
- window.clearTimeout(this.timerId);
- }
-
- private async fetchLastCrawl() {
- if (!this.lastCrawlId) return;
-
- let crawlState: CrawlState | null = null;
-
- try {
- const { stats, pageCount, reviewStatus, state } = await this.getCrawl(
- this.lastCrawlId,
- );
- this.lastCrawl = { stats, pageCount, reviewStatus };
-
- crawlState = state;
- } catch {
- this.notify.toast({
- message: msg("Sorry, couldn't retrieve latest crawl at this time."),
- variant: "danger",
- icon: "exclamation-octagon",
- id: "data-retrieve-error",
- });
+ if (this.pollTask.value) {
+ window.clearTimeout(this.pollTask.value);
}
- if (
- !this.logTotals ||
- (crawlState && isActive({ state: crawlState })) ||
- this.workflowTab === WorkflowTab.Logs
- ) {
- try {
- this.logTotals = await this.getLogTotals(this.lastCrawlId);
- } catch (err) {
- // Fail silently, since we're fetching just the total
- console.debug(err);
- }
- }
+ this.pollTask.abort();
}
- private async getCrawl(crawlId: Crawl["id"]): Promise {
+ private async getCrawl(crawlId: Crawl["id"], signal: AbortSignal) {
const data = await this.api.fetch(
`/orgs/${this.orgId}/crawls/${crawlId}/replay.json`,
+ { signal },
);
return data;
}
- private async getLogTotals(
- crawlId: Crawl["id"],
- ): Promise {
+ private async getLogTotals(crawlId: Crawl["id"], signal: AbortSignal) {
const query = queryString.stringify({ pageSize: 1 });
const [errors, behaviors] = await Promise.all([
this.api.fetch>(
`/orgs/${this.orgId}/crawls/${crawlId}/errors?${query}`,
+ { signal },
),
this.api.fetch>(
`/orgs/${this.orgId}/crawls/${crawlId}/behaviorLogs?${query}`,
+ { signal },
),
]);
@@ -1631,8 +2202,8 @@ export class WorkflowDetail extends BtrixElement {
* Create a new template using existing template data
*/
private async duplicateConfig() {
- if (!this.workflow) await this.getWorkflowPromise;
- if (!this.seeds) await this.getSeedsPromise;
+ if (!this.workflow) await this.workflowTask.taskComplete;
+ if (!this.seeds) await this.seedsTask.taskComplete;
await this.updateComplete;
if (!this.workflow) return;
@@ -1685,56 +2256,63 @@ export class WorkflowDetail extends BtrixElement {
}
}
- private async pauseResume() {
+ private async pauseResume(signal: AbortSignal) {
if (!this.lastCrawlId) return;
- const pause = this.workflow?.lastCrawlState !== "paused";
+ const shouldPause = !this.isPaused;
try {
const data = await this.api.fetch<{ success: boolean }>(
- `/orgs/${this.orgId}/crawls/${this.lastCrawlId}/${pause ? "pause" : "resume"}`,
+ `/orgs/${this.orgId}/crawls/${this.lastCrawlId}/${shouldPause ? "pause" : "resume"}`,
{
method: "POST",
+ signal,
},
);
if (data.success) {
- void this.fetchWorkflow();
+ this.notify.toast({
+ message: shouldPause
+ ? msg("Pausing crawl.")
+ : msg("Resuming paused crawl."),
+ variant: "success",
+ icon: "check2-circle",
+ id: "crawl-action-status",
+ });
} else {
throw data;
}
-
- this.notify.toast({
- message: pause ? msg("Pausing crawl.") : msg("Resuming paused crawl."),
- variant: "success",
- icon: "check2-circle",
- id: "crawl-pause-resume-status",
- });
} catch {
this.notify.toast({
- message: pause
+ message: shouldPause
? msg("Something went wrong, couldn't pause crawl.")
: msg("Something went wrong, couldn't resume paused crawl."),
variant: "danger",
icon: "exclamation-octagon",
- id: "crawl-pause-resume-status",
+ id: "crawl-action-status",
});
}
}
- private async cancel() {
+ private async cancel(signal: AbortSignal) {
if (!this.lastCrawlId) return;
- this.isCancelingOrStoppingCrawl = true;
+ this.isCancelingRun = true;
try {
const data = await this.api.fetch<{ success: boolean }>(
`/orgs/${this.orgId}/crawls/${this.lastCrawlId}/cancel`,
{
method: "POST",
+ signal,
},
);
if (data.success) {
- void this.fetchWorkflow();
+ this.notify.toast({
+ message: msg("Canceling crawl."),
+ variant: "success",
+ icon: "check2-circle",
+ id: "crawl-action-status",
+ });
} else {
throw data;
}
@@ -1743,27 +2321,29 @@ export class WorkflowDetail extends BtrixElement {
message: msg("Something went wrong, couldn't cancel crawl."),
variant: "danger",
icon: "exclamation-octagon",
- id: "crawl-stop-error",
+ id: "crawl-action-status",
});
}
-
- this.isCancelingOrStoppingCrawl = false;
}
- private async stop() {
+ private async stop(signal: AbortSignal) {
if (!this.lastCrawlId) return;
- this.isCancelingOrStoppingCrawl = true;
-
try {
const data = await this.api.fetch<{ success: boolean }>(
`/orgs/${this.orgId}/crawls/${this.lastCrawlId}/stop`,
{
method: "POST",
+ signal,
},
);
if (data.success) {
- void this.fetchWorkflow();
+ this.notify.toast({
+ message: msg("Stopping crawl."),
+ variant: "success",
+ icon: "check2-circle",
+ id: "crawl-action-status",
+ });
} else {
throw data;
}
@@ -1772,31 +2352,29 @@ export class WorkflowDetail extends BtrixElement {
message: msg("Something went wrong, couldn't stop crawl."),
variant: "danger",
icon: "exclamation-octagon",
- id: "crawl-stop-error",
+ id: "crawl-action-status",
});
}
-
- this.isCancelingOrStoppingCrawl = false;
}
- private async runNow(): Promise {
+ private async runNow(signal: AbortSignal): Promise {
try {
const data = await this.api.fetch<{ started: string | null }>(
`/orgs/${this.orgId}/crawlconfigs/${this.workflowId}/run`,
{
method: "POST",
+ signal,
},
);
this.lastCrawlId = data.started;
- this.lastCrawlStartTime = new Date().toISOString();
- void this.fetchWorkflow();
+
this.navigate.to(`${this.basePath}/${WorkflowTab.LatestCrawl}`);
this.notify.toast({
message: msg("Starting crawl."),
variant: "success",
icon: "check2-circle",
- id: "crawl-start-status",
+ id: "crawl-action-status",
});
} catch (e) {
let message = msg("Sorry, couldn't run crawl at this time.");
@@ -1819,7 +2397,7 @@ export class WorkflowDetail extends BtrixElement {
message: message,
variant: "danger",
icon: "exclamation-octagon",
- id: "crawl-start-status",
+ id: "crawl-action-status",
});
}
}
@@ -1838,20 +2416,17 @@ export class WorkflowDetail extends BtrixElement {
}),
});
this.crawlToDelete = null;
- this.crawls = {
- ...this.crawls!,
- items: this.crawls!.items.filter((c) => c.id !== crawl.id),
- };
+ void this.crawlsTask.run();
+
this.notify.toast({
message: msg(`Successfully deleted crawl`),
variant: "success",
icon: "check2-circle",
id: "archived-item-delete-status",
});
- void this.fetchCrawls();
// Update crawl count
- void this.fetchWorkflow();
+ void this.workflowTask.run();
} catch (e) {
if (this.crawlToDelete) {
this.confirmDeleteCrawl(this.crawlToDelete);
diff --git a/frontend/src/strings/archived-items/tooltips.ts b/frontend/src/strings/archived-items/tooltips.ts
deleted file mode 100644
index 9705de3a35..0000000000
--- a/frontend/src/strings/archived-items/tooltips.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { msg } from "@lit/localize";
-
-export const tooltipFor = {
- downloadMultWacz: msg(msg("Download Files as Multi-WACZ")),
- downloadLogs: msg("Download Entire Log File"),
-};
diff --git a/frontend/src/theme.stylesheet.css b/frontend/src/theme.stylesheet.css
index 8684c0afce..34044e81e8 100644
--- a/frontend/src/theme.stylesheet.css
+++ b/frontend/src/theme.stylesheet.css
@@ -158,6 +158,12 @@
font-size: var(--sl-input-help-text-font-size-medium);
}
+ /* TODO Move to custom button */
+ sl-button[size="small"].micro::part(base) {
+ --sl-input-height-small: 1.5rem;
+ font-size: var(--sl-font-size-x-small);
+ }
+
/* Update button colors */
sl-button[variant="primary"]:not([outline])::part(base) {
background-color: theme(colors.primary.400);
diff --git a/frontend/src/types/crawler.ts b/frontend/src/types/crawler.ts
index a3816449b8..f348bb5559 100644
--- a/frontend/src/types/crawler.ts
+++ b/frontend/src/types/crawler.ts
@@ -92,7 +92,11 @@ export type Workflow = CrawlConfig & {
lastCrawlSize: number | null;
lastStartedByName: string | null;
lastCrawlStopping: boolean | null;
+ // User has requested pause, but actual state can be running or paused
+ // OR user has requested resume, but actual state is not running
lastCrawlShouldPause: boolean | null;
+ lastCrawlPausedAt: string | null;
+ lastCrawlPausedExpiry: string | null;
lastRun: string;
totalSize: string | null;
inactive: boolean;
diff --git a/frontend/src/utils/executionTimeFormatter.test.ts b/frontend/src/utils/executionTimeFormatter.test.ts
index c5318b97cf..f4e178e07b 100644
--- a/frontend/src/utils/executionTimeFormatter.test.ts
+++ b/frontend/src/utils/executionTimeFormatter.test.ts
@@ -12,6 +12,9 @@ describe("formatHours", () => {
it("returns 1m when given a time under a minute", () => {
expect(humanizeSeconds(24, "en-US")).to.equal("1m");
});
+ it("returns seconds given a time under a minute when not rounding", () => {
+ expect(humanizeSeconds(24, "en-US")).to.equal("1m");
+ });
it("returns 0m and seconds when given a time under a minute with seconds on", () => {
expect(humanizeSeconds(24, "en-US", true)).to.equal("0m 24s");
});
@@ -105,7 +108,20 @@ describe("humanizeExecutionSeconds", () => {
parentNode,
},
);
- expect(el.textContent?.trim()).to.equal("1 minute\u00a0(0m 24s)");
- expect(parentNode.innerText).to.equal("1 minute\u00a0(0m 24s)");
+ expect(el.textContent?.trim()).to.equal("<1 minute\u00a0(0m 24s)");
+ expect(parentNode.innerText).to.equal("<1 minute\u00a0(0m 24s)");
+ });
+ it("formats zero seconds", async () => {
+ const parentNode = document.createElement("div");
+ const el = await fixture(
+ humanizeExecutionSeconds(0, {
+ displaySeconds: true,
+ }),
+ {
+ parentNode,
+ },
+ );
+ expect(el.textContent?.trim()).to.equal("0 minutes");
+ expect(parentNode.innerText).to.equal("0 minutes");
});
});
diff --git a/frontend/src/utils/executionTimeFormatter.ts b/frontend/src/utils/executionTimeFormatter.ts
index ee8e7d8a54..d80afdc783 100644
--- a/frontend/src/utils/executionTimeFormatter.ts
+++ b/frontend/src/utils/executionTimeFormatter.ts
@@ -88,7 +88,7 @@ export const humanizeExecutionSeconds = (
) => {
const {
style = "long",
- displaySeconds = false,
+ displaySeconds = seconds < 60,
round = "up",
} = options || {};
const locale = localize.activeLanguage;
@@ -119,6 +119,7 @@ export const humanizeExecutionSeconds = (
: Math.floor(seconds / 60) === 0 && seconds % 60 !== 0;
const formattedDetails =
detailsRelevant || seconds > 3600 ? `\u00a0(${details})` : nothing;
+ const prefix = detailsRelevant && seconds < 60 ? "<" : "";
switch (style) {
case "long":
@@ -126,13 +127,12 @@ export const humanizeExecutionSeconds = (
title="${ifDefined(
fullMinutes !== compactMinutes ? fullMinutes : undefined,
)}"
- >
- ${compactMinutes}${formattedDetails}${prefix}${compactMinutes}${formattedDetails}`;
case "short":
return html`${compactMinutes}${prefix}${compactMinutes}`;
}
};