Re-run crawl from detail view + handle inactive crawl template (#268)

closes #253
This commit is contained in:
sua yoo 2022-06-29 14:17:09 -07:00 committed by GitHub
parent d144591dbf
commit 92292591ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 207 additions and 50 deletions

View File

@ -7,8 +7,7 @@ import { RelativeDuration } from "../../components/relative-duration";
import type { AuthState } from "../../utils/AuthService"; import type { AuthState } from "../../utils/AuthService";
import LiteElement, { html } from "../../utils/LiteElement"; import LiteElement, { html } from "../../utils/LiteElement";
import { CopyButton } from "../../components/copy-button"; import { CopyButton } from "../../components/copy-button";
import type { Crawl } from "./types"; import type { Crawl, CrawlTemplate } from "./types";
import { times } from "lodash";
type SectionName = "overview" | "watch" | "replay" | "files" | "logs"; type SectionName = "overview" | "watch" | "replay" | "files" | "logs";
@ -42,6 +41,9 @@ export class CrawlDetail extends LiteElement {
@state() @state()
private crawl?: Crawl; private crawl?: Crawl;
@state()
private crawlTemplate?: CrawlTemplate;
@state() @state()
private sectionName: SectionName = "overview"; private sectionName: SectionName = "overview";
@ -77,15 +79,22 @@ export class CrawlDetail extends LiteElement {
return this.crawl.resources.length > 0; return this.crawl.resources.length > 0;
} }
async firstUpdated() { firstUpdated() {
if (!this.crawlsBaseUrl) { if (!this.crawlsBaseUrl) {
throw new Error("Crawls base URL not defined"); throw new Error("Crawls base URL not defined");
} }
this.fetchCrawl(); this.fetchData();
} }
updated(changedProperties: Map<string, any>) { updated(changedProperties: Map<string, any>) {
const prevId = changedProperties.get("crawlId");
if (prevId && prevId !== this.crawlId) {
// Handle update on URL change, e.g. from re-run
this.stopPollTimer();
this.fetchData();
} else {
const prevCrawl = changedProperties.get("crawl"); const prevCrawl = changedProperties.get("crawl");
if (prevCrawl && this.crawl) { if (prevCrawl && this.crawl) {
@ -94,6 +103,7 @@ export class CrawlDetail extends LiteElement {
} }
} }
} }
}
connectedCallback(): void { connectedCallback(): void {
// Set initial active section based on URL #hash value // Set initial active section based on URL #hash value
@ -302,6 +312,28 @@ export class CrawlDetail extends LiteElement {
> >
<ul class="text-sm text-0-800 whitespace-nowrap" role="menu"> <ul class="text-sm text-0-800 whitespace-nowrap" role="menu">
${!this.isActive && this.crawlTemplate && !this.crawlTemplate.inactive
? html`
<li
class="p-2 text-purple-500 hover:bg-purple-500 hover:text-white cursor-pointer"
role="menuitem"
@click=${(e: any) => {
this.runNow();
e.target.closest("sl-dropdown").hide();
}}
>
<sl-icon
class="inline-block align-middle"
name="arrow-clockwise"
></sl-icon>
<span class="inline-block align-middle">
${msg("Re-run crawl")}
</span>
</li>
<hr />
`
: ""}
<li <li
class="p-2 hover:bg-zinc-100 cursor-pointer" class="p-2 hover:bg-zinc-100 cursor-pointer"
role="menuitem" role="menuitem"
@ -608,6 +640,11 @@ export class CrawlDetail extends LiteElement {
<span class="inline-block align-middle"> <span class="inline-block align-middle">
${this.crawl.configName} ${this.crawl.configName}
</span> </span>
${this.crawlTemplate?.inactive
? html`<sl-tag type="warning" size="small"
>${msg("Inactive")}</sl-tag
>`
: ""}
</a> </a>
` `
: html`<sl-skeleton class="h-6"></sl-skeleton>`} : html`<sl-skeleton class="h-6"></sl-skeleton>`}
@ -743,6 +780,11 @@ export class CrawlDetail extends LiteElement {
`; `;
} }
private async fetchData() {
await this.fetchCrawl();
this.fetchCrawlTemplate();
}
/** /**
* Fetch crawl and update internal state * Fetch crawl and update internal state
*/ */
@ -751,7 +793,8 @@ export class CrawlDetail extends LiteElement {
this.crawl = await this.getCrawl(); this.crawl = await this.getCrawl();
if (this.isActive) { if (this.isActive) {
// Start timer for next poll // Restart timer for next poll
this.stopPollTimer();
this.timerId = window.setTimeout(() => { this.timerId = window.setTimeout(() => {
this.fetchCrawl(); this.fetchCrawl();
}, 1000 * POLL_INTERVAL_SECONDS); }, 1000 * POLL_INTERVAL_SECONDS);
@ -768,12 +811,6 @@ export class CrawlDetail extends LiteElement {
} }
async getCrawl(): Promise<Crawl> { async getCrawl(): Promise<Crawl> {
// Mock to use in dev:
// return import("../../__mocks__/api/archives/[id]/crawls").then(
// (module) => module.default.running[0]
// // (module) => module.default.finished[0]
// );
const data: Crawl = await this.apiFetch( const data: Crawl = await this.apiFetch(
`${this.crawlsAPIBaseUrl || this.crawlsBaseUrl}/${this.crawlId}.json`, `${this.crawlsAPIBaseUrl || this.crawlsBaseUrl}/${this.crawlId}.json`,
this.authState! this.authState!
@ -782,6 +819,30 @@ export class CrawlDetail extends LiteElement {
return data; return data;
} }
/**
* Fetch crawl template and update internal state
*/
private async fetchCrawlTemplate(): Promise<void> {
try {
this.crawlTemplate = await this.getCrawlTemplate();
} catch {
// Fail silently since page will mostly still function
}
}
async getCrawlTemplate(): Promise<CrawlTemplate> {
if (!this.crawl) {
throw new Error("missing crawl");
}
const data: CrawlTemplate = await this.apiFetch(
`/archives/${this.crawl.aid}/crawlconfigs/${this.crawl.cid}`,
this.authState!
);
return data;
}
private async cancel() { private async cancel() {
if (window.confirm(msg("Are you sure you want to cancel the crawl?"))) { if (window.confirm(msg("Are you sure you want to cancel the crawl?"))) {
const data = await this.apiFetch( const data = await this.apiFetch(
@ -864,6 +925,63 @@ export class CrawlDetail extends LiteElement {
this.isSubmittingUpdate = false; this.isSubmittingUpdate = false;
} }
private async runNow() {
if (!this.crawl) return;
try {
// Get crawl config to check if crawl is already running
const crawlTemplate = await this.getCrawlTemplate();
if (crawlTemplate.currCrawlId) {
this.notify({
message: msg(
html`Crawl of <strong>${this.crawl.configName}</strong> is already
running.
<br />
<a
class="underline hover:no-underline"
href="/archives/${this.crawl
.aid}/crawls/crawl/${crawlTemplate.currCrawlId}"
@click=${this.navLink.bind(this)}
>View crawl</a
>`
),
type: "warning",
icon: "exclamation-triangle",
});
return;
}
const data = await this.apiFetch(
`/archives/${this.crawl.aid}/crawlconfigs/${this.crawl.cid}/run`,
this.authState!,
{
method: "POST",
}
);
if (data.started) {
this.navTo(`/archives/${this.crawl.aid}/crawls/crawl/${data.started}`);
}
this.notify({
message: msg(
html`Started crawl from <strong>${this.crawl.configName}</strong>.`
),
type: "success",
icon: "check2-circle",
duration: 8000,
});
} catch {
this.notify({
message: msg("Sorry, couldn't run crawl at this time."),
type: "danger",
icon: "exclamation-octagon",
});
}
}
private stopPollTimer() { private stopPollTimer() {
window.clearTimeout(this.timerId); window.clearTimeout(this.timerId);
} }

View File

@ -1125,6 +1125,11 @@ export class CrawlTemplatesDetail extends LiteElement {
} }
); );
this.crawlTemplate = {
...this.crawlTemplate,
inactive: true,
};
this.notify({ this.notify({
message: msg( message: msg(
html`Deactivated <strong>${this.crawlTemplate.name}</strong>.` html`Deactivated <strong>${this.crawlTemplate.name}</strong>.`

View File

@ -11,6 +11,7 @@ import { RelativeDuration } from "../../components/relative-duration";
import type { AuthState } from "../../utils/AuthService"; import type { AuthState } from "../../utils/AuthService";
import LiteElement, { html } from "../../utils/LiteElement"; import LiteElement, { html } from "../../utils/LiteElement";
import type { Crawl, CrawlTemplate } from "./types"; import type { Crawl, CrawlTemplate } from "./types";
import type { InitialCrawlTemplate } from "./crawl-templates-new";
type CrawlSearchResult = { type CrawlSearchResult = {
item: Crawl; item: Crawl;
@ -49,9 +50,6 @@ export class CrawlsList extends LiteElement {
@property({ type: Object }) @property({ type: Object })
authState!: AuthState; authState!: AuthState;
@property({ type: String })
archiveId?: string;
// e.g. `/archive/${this.archiveId}/crawls` // e.g. `/archive/${this.archiveId}/crawls`
@property({ type: String }) @property({ type: String })
crawlsBaseUrl!: string; crawlsBaseUrl!: string;
@ -571,20 +569,17 @@ export class CrawlsList extends LiteElement {
} }
private async runNow(crawl: Crawl) { private async runNow(crawl: Crawl) {
try {
// Get crawl config to check if crawl is already running // Get crawl config to check if crawl is already running
const crawlTemplate = await this.getCrawlTemplate(crawl); const crawlTemplate = await this.getCrawlTemplate(crawl);
if (crawlTemplate.currCrawlId) { if (crawlTemplate?.currCrawlId) {
this.notify({ this.notify({
message: msg( message: msg(
html`Crawl of <strong>${crawl.configName}</strong> is already html`Crawl of <strong>${crawl.configName}</strong> is already running.
running.
<br /> <br />
<a <a
class="underline hover:no-underline" class="underline hover:no-underline"
href="/archives/${this href="/archives/${crawl.aid}/crawls/crawl/${crawlTemplate.currCrawlId}"
.archiveId}/crawls/crawl/${crawlTemplate.currCrawlId}"
@click=${this.navLink.bind(this)} @click=${this.navLink.bind(this)}
>View crawl</a >View crawl</a
>` >`
@ -596,6 +591,7 @@ export class CrawlsList extends LiteElement {
return; return;
} }
try {
const data = await this.apiFetch( const data = await this.apiFetch(
`/archives/${crawl.aid}/crawlconfigs/${crawl.cid}/run`, `/archives/${crawl.aid}/crawlconfigs/${crawl.cid}/run`,
this.authState!, this.authState!,
@ -614,8 +610,7 @@ export class CrawlsList extends LiteElement {
<br /> <br />
<a <a
class="underline hover:no-underline" class="underline hover:no-underline"
href="/archives/${this href="/archives/${crawl.aid}/crawls/crawl/${data.started}#watch"
.archiveId}/crawls/crawl/${data.started}#watch"
@click=${this.navLink.bind(this)} @click=${this.navLink.bind(this)}
>Watch crawl</a >Watch crawl</a
>` >`
@ -624,7 +619,24 @@ export class CrawlsList extends LiteElement {
icon: "check2-circle", icon: "check2-circle",
duration: 8000, duration: 8000,
}); });
} catch { } catch (e: any) {
if (e.isApiError && e.statusCode === 404) {
this.notify({
message: msg(
html`Sorry, cannot rerun crawl from a deactivated crawl template.
<br />
<button
class="underline hover:no-underline"
@click=${() => this.duplicateConfig(crawl, crawlTemplate)}
>
Duplicate crawl template
</button>`
),
type: "danger",
icon: "exclamation-octagon",
duration: 8000,
});
} else {
this.notify({ this.notify({
message: msg("Sorry, couldn't run crawl at this time."), message: msg("Sorry, couldn't run crawl at this time."),
type: "danger", type: "danger",
@ -632,6 +644,7 @@ export class CrawlsList extends LiteElement {
}); });
} }
} }
}
async getCrawlTemplate(crawl: Crawl): Promise<CrawlTemplate> { async getCrawlTemplate(crawl: Crawl): Promise<CrawlTemplate> {
const data: CrawlTemplate = await this.apiFetch( const data: CrawlTemplate = await this.apiFetch(
@ -641,6 +654,27 @@ export class CrawlsList extends LiteElement {
return data; return data;
} }
/**
* Create a new template using existing template data
*/
private async duplicateConfig(crawl: Crawl, template: CrawlTemplate) {
const crawlTemplate: InitialCrawlTemplate = {
name: msg(str`${template.name} Copy`),
config: template.config,
profileid: template.profileid || null,
};
this.navTo(`/archives/${crawl.aid}/crawl-templates/new`, {
crawlTemplate,
});
this.notify({
message: msg(str`Copied crawl configuration to new template.`),
type: "success",
icon: "check2-circle",
});
}
} }
customElements.define("btrix-crawls-list", CrawlsList); customElements.define("btrix-crawls-list", CrawlsList);