Run and delete crawl templates from list view (#94)
This commit is contained in:
parent
b506442b21
commit
9ed216ba05
@ -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);
|
||||||
|
@ -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,
|
||||||
|
@ -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 }>
|
||||||
) {
|
) {
|
||||||
|
@ -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;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user