Run and delete crawl templates from list view (#94)

This commit is contained in:
sua yoo 2022-01-22 14:18:02 -08:00 committed by GitHub
parent b506442b21
commit 9ed216ba05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 302 additions and 49 deletions

View File

@ -1,9 +1,26 @@
import { state, property } from "lit/decorators.js"; import { state, property } from "lit/decorators.js";
import { msg, localized, str } from "@lit/localize"; import { msg, localized, str } from "@lit/localize";
import cronParser from "cron-parser";
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 { CrawlTemplate } from "./types"; import type { CrawlConfig } from "./types";
type CrawlTemplate = {
id: string;
name: string;
schedule: string;
user: string;
crawlCount: number;
lastCrawlId: string;
lastCrawlTime: string;
currCrawlId: string;
config: CrawlConfig;
};
type RunningCrawlsMap = {
/** Map of configId: crawlId */
[configId: string]: string;
};
/** /**
* Usage: * Usage:
@ -19,31 +36,289 @@ export class CrawlTemplatesList extends LiteElement {
@property({ type: String }) @property({ type: String })
archiveId!: string; archiveId!: string;
@property({ type: Array }) @state()
crawlTemplates?: CrawlTemplate[]; crawlTemplates?: CrawlTemplate[];
@state() @state()
private serverError?: string; runningCrawlsMap: RunningCrawlsMap = {};
private get timeZone() {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
}
async firstUpdated() {
try {
this.crawlTemplates = await this.getCrawlTemplates();
if (!this.crawlTemplates.length) {
this.navTo(`/archives/${this.archiveId}/crawl-templates/new`);
}
} catch (e) {
this.notify({
message: msg("Sorry, couldn't retrieve crawl templates at this time."),
type: "danger",
icon: "exclamation-octagon",
duration: 10000,
});
}
}
render() { render() {
if (!this.crawlTemplates) {
return html`<div
class="w-full flex items-center justify-center my-24 text-4xl"
>
<sl-spinner></sl-spinner>
</div>`;
}
return html` return html`
<div class="text-center"> <div class="text-center"></div>
<sl-button
<div
class=${this.crawlTemplates.length
? "grid sm:grid-cols-2 md:grid-cols-3 gap-4 mb-4"
: "flex justify-center"}
>
<div
class="col-span-1 bg-slate-50 border border-dotted border-primary text-primary rounded px-6 py-4 flex items-center justify-center"
@click=${() => @click=${() =>
this.navTo(`/archives/${this.archiveId}/crawl-templates/new`)} this.navTo(`/archives/${this.archiveId}/crawl-templates/new`)}
role="button"
> >
<sl-icon slot="prefix" name="plus-square-dotted"></sl-icon> <sl-icon class="mr-2" name="plus-square-dotted"></sl-icon>
${msg("Create new crawl template")} <span
</sl-button> class="mr-2 ${this.crawlTemplates.length
? "text-sm"
: "font-medium"}"
>${msg("Create New Crawl Template")}</span
>
</div>
</div> </div>
<div> <div class="grid sm:grid-cols-2 md:grid-cols-3 gap-4">
${this.crawlTemplates?.map( ${this.crawlTemplates.map(
(template) => html`<div>${template.id}</div>` (t) =>
html`<div
class="col-span-1 p-1 border hover:border-indigo-200 rounded text-sm transition-colors"
aria-label=${t.name}
>
<header class="flex">
<a
href=${`/archives/${this.archiveId}/crawl-templates/${t.id}`}
class="block flex-1 px-3 pt-3 font-medium hover:underline whitespace-nowrap truncate mb-1"
title=${t.name}
@click=${this.navLink}
>
${t.name || "?"}
</a>
<sl-dropdown>
<sl-icon-button
slot="trigger"
name="three-dots-vertical"
label="More"
style="font-size: 1rem"
></sl-icon-button>
<ul role="menu">
<li
class="px-4 py-2 text-danger hover:bg-danger hover:text-white cursor-pointer"
role="menuitem"
@click=${(e: any) => {
// Close dropdown before deleting template
e.target.closest("sl-dropdown").hide();
this.deleteTemplate(t);
}}
>
${msg("Delete")}
</li>
</ul>
</sl-dropdown>
</header>
<div class="px-3 pb-3 flex justify-between items-end">
<div class="grid gap-1 text-xs">
<div
class="font-mono whitespace-nowrap truncate text-gray-500"
title=${t.config.seeds.join(", ")}
>
${t.config.seeds.join(", ")}
</div>
<div class="font-mono text-purple-500">
${t.crawlCount === 1
? msg(str`${t.crawlCount} crawl`)
: msg(
str`${(t.crawlCount || 0).toLocaleString()} crawls`
)}
</div>
<div class="text-gray-500">
${msg(html`Last:
<span
><sl-format-date
date=${t.lastCrawlTime}
month="2-digit"
day="2-digit"
year="2-digit"
hour="numeric"
minute="numeric"
time-zone=${this.timeZone}
></sl-format-date
></span>`)}
</div>
<div class="text-gray-500">
${t.schedule
? msg(html`Next:
<sl-format-date
date="${cronParser
.parseExpression(t.schedule, {
utc: true,
})
.next()
.toString()}"
month="2-digit"
day="2-digit"
year="2-digit"
hour="numeric"
minute="numeric"
time-zone=${this.timeZone}
></sl-format-date>`)
: html`<span class="text-gray-400"
>${msg("No schedule")}</span
>`}
</div>
</div>
<div>
<button
class="text-xs border rounded-sm px-2 h-7 ${this
.runningCrawlsMap[t.id]
? "bg-purple-50"
: "bg-white"} border-purple-200 hover:border-purple-500 text-purple-600 transition-colors"
@click=${() =>
this.runningCrawlsMap[t.id]
? this.navTo(
`/archives/${this.archiveId}/crawls/${
this.runningCrawlsMap[t.id]
}`
)
: this.runNow(t)}
>
<span>
${this.runningCrawlsMap[t.id]
? msg("View crawl")
: msg("Run now")}
</span>
</button>
</div>
</div>
</div>`
)} )}
</div> </div>
`; `;
} }
/**
* Fetch crawl templates and record running crawls
* associated with the crawl templates
**/
private async getCrawlTemplates(): Promise<CrawlTemplate[]> {
type CrawlConfig = Omit<CrawlTemplate, "config"> & {
config: Omit<CrawlTemplate["config"], "seeds"> & {
seeds: (string | { url: string })[];
};
};
const data: { crawlConfigs: CrawlConfig[] } = await this.apiFetch(
`/archives/${this.archiveId}/crawlconfigs`,
this.authState!
);
const crawlConfigs: CrawlTemplate[] = [];
const runningCrawlsMap: RunningCrawlsMap = {};
data.crawlConfigs.forEach(({ config, ...configMeta }) => {
crawlConfigs.push({
...configMeta,
config: {
...config,
// Flatten seeds into array of URL strings
seeds: config.seeds.map((seed) =>
typeof seed === "string" ? seed : seed.url
),
},
});
if (configMeta.currCrawlId) {
runningCrawlsMap[configMeta.id] = configMeta.currCrawlId;
}
});
this.runningCrawlsMap = runningCrawlsMap;
return crawlConfigs;
}
private async deleteTemplate(template: CrawlTemplate): Promise<void> {
try {
await this.apiFetch(
`/archives/${this.archiveId}/crawlconfigs/${template.id}`,
this.authState!,
{
method: "DELETE",
}
);
this.notify({
message: msg(str`Deleted <strong>${template.name}</strong>.`),
type: "success",
icon: "check2-circle",
});
this.crawlTemplates = this.crawlTemplates!.filter(
(t) => t.id !== template.id
);
} catch {
this.notify({
message: msg("Sorry, couldn't delete crawl template at this time."),
type: "danger",
icon: "exclamation-octagon",
});
}
}
private async runNow(template: CrawlTemplate): Promise<void> {
try {
const data = await this.apiFetch(
`/archives/${this.archiveId}/crawlconfigs/${template.id}/run`,
this.authState!,
{
method: "POST",
}
);
const crawlId = data.started;
this.runningCrawlsMap = {
...this.runningCrawlsMap,
[template.id]: crawlId,
};
this.notify({
message: msg(
str`Started crawl from <strong>${template.name}</strong>. <br /><a class="underline hover:no-underline" href="/archives/${this.archiveId}/crawls/${data.run_now_job}">View crawl</a>`
),
type: "success",
icon: "check2-circle",
duration: 10000,
});
} catch {
this.notify({
message: msg("Sorry, couldn't run crawl at this time."),
type: "danger",
icon: "exclamation-octagon",
});
}
}
} }
customElements.define("btrix-crawl-templates-list", CrawlTemplatesList); customElements.define("btrix-crawl-templates-list", CrawlTemplatesList);

View File

@ -5,7 +5,16 @@ import cronParser from "cron-parser";
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 { getLocaleTimeZone } from "../../utils/localization"; import { getLocaleTimeZone } from "../../utils/localization";
import type { CrawlTemplate } from "./types"; import type { CrawlConfig } from "./types";
export type NewCrawlTemplate = {
id?: string;
name: string;
schedule: string;
runNow: boolean;
crawlTimeout?: number;
config: CrawlConfig;
};
const initialValues = { const initialValues = {
name: "", name: "",
@ -448,7 +457,7 @@ export class CrawlTemplatesNew extends LiteElement {
const crawlTimeoutMinutes = formData.get("crawlTimeoutMinutes"); const crawlTimeoutMinutes = formData.get("crawlTimeoutMinutes");
const pageLimit = formData.get("limit"); const pageLimit = formData.get("limit");
const seedUrlsStr = formData.get("seedUrls"); const seedUrlsStr = formData.get("seedUrls");
const template: Partial<CrawlTemplate> = { const template: Partial<NewCrawlTemplate> = {
name: formData.get("name") as string, name: formData.get("name") as string,
schedule: this.getUTCSchedule(), schedule: this.getUTCSchedule(),
runNow: this.isRunNow, runNow: this.isRunNow,

View File

@ -7,7 +7,6 @@ import type { ArchiveData } from "../../utils/archives";
import LiteElement, { html } from "../../utils/LiteElement"; import LiteElement, { html } from "../../utils/LiteElement";
import { needLogin } from "../../utils/auth"; import { needLogin } from "../../utils/auth";
import { isOwner } from "../../utils/archives"; import { isOwner } from "../../utils/archives";
import type { CrawlTemplate } from "./types";
import "./crawl-templates-list"; import "./crawl-templates-list";
import "./crawl-templates-new"; import "./crawl-templates-new";
@ -40,9 +39,6 @@ export class Archive extends LiteElement {
@state() @state()
private archive?: ArchiveData; private archive?: ArchiveData;
@state()
private crawlTemplates?: CrawlTemplate[];
@state() @state()
private successfullyInvitedEmail?: string; private successfullyInvitedEmail?: string;
@ -61,17 +57,7 @@ export class Archive extends LiteElement {
} }
async updated(changedProperties: any) { async updated(changedProperties: any) {
if ( if (changedProperties.has("isAddingMember") && this.isAddingMember) {
changedProperties.has("archiveTab") &&
this.archiveTab === "crawl-templates" &&
!this.isNewResourceTab
) {
this.crawlTemplates = await this.getCrawlTemplates();
if (!this.crawlTemplates.length) {
this.navTo(`/archives/${this.archiveId}/crawl-templates/new`);
}
} else if (changedProperties.has("isAddingMember") && this.isAddingMember) {
this.successfullyInvitedEmail = undefined; this.successfullyInvitedEmail = undefined;
} }
} }
@ -178,7 +164,6 @@ export class Archive extends LiteElement {
return html`<btrix-crawl-templates-list return html`<btrix-crawl-templates-list
.authState=${this.authState!} .authState=${this.authState!}
.archiveId=${this.archiveId!} .archiveId=${this.archiveId!}
.crawlTemplates=${this.crawlTemplates}
></btrix-crawl-templates-list>`; ></btrix-crawl-templates-list>`;
} }
@ -267,15 +252,6 @@ export class Archive extends LiteElement {
return data; return data;
} }
async getCrawlTemplates(): Promise<CrawlTemplate[]> {
const data = await this.apiFetch(
`/archives/${this.archiveId}/crawlconfigs`,
this.authState!
);
return data.crawl_configs;
}
onInviteSuccess( onInviteSuccess(
event: CustomEvent<{ inviteEmail: string; isExistingUser: boolean }> event: CustomEvent<{ inviteEmail: string; isExistingUser: boolean }>
) { ) {

View File

@ -1,12 +1,5 @@
export type CrawlTemplate = { export type CrawlConfig = {
id?: string; seeds: string[];
name: string; scopeType?: string;
schedule: string; limit?: number;
runNow: boolean;
crawlTimeout?: number;
config: {
seeds: string[];
scopeType?: string;
limit?: number;
};
}; };