Filter and sort crawl templates (#217)
This commit is contained in:
parent
cb80c6767e
commit
f157e2031f
@ -49,7 +49,7 @@ export class BrowserProfilesList extends LiteElement {
|
|||||||
return html`<header class="mb-3 text-right">
|
return html`<header class="mb-3 text-right">
|
||||||
<a
|
<a
|
||||||
href=${`/archives/${this.archiveId}/browser-profiles/new`}
|
href=${`/archives/${this.archiveId}/browser-profiles/new`}
|
||||||
class="inline-block bg-primary hover:bg-indigo-400 text-white text-center font-medium leading-none rounded px-3 py-2 transition-colors"
|
class="inline-block bg-indigo-500 hover:bg-indigo-400 text-white text-center font-medium leading-none rounded px-3 py-2 transition-colors"
|
||||||
role="button"
|
role="button"
|
||||||
@click=${this.navLink}
|
@click=${this.navLink}
|
||||||
>
|
>
|
||||||
|
@ -2,6 +2,12 @@ import type { HTMLTemplateResult } from "lit";
|
|||||||
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 cronParser from "cron-parser";
|
||||||
|
import debounce from "lodash/fp/debounce";
|
||||||
|
import flow from "lodash/fp/flow";
|
||||||
|
import map from "lodash/fp/map";
|
||||||
|
import orderBy from "lodash/fp/orderBy";
|
||||||
|
import filter from "lodash/fp/filter";
|
||||||
|
import Fuse from "fuse.js";
|
||||||
|
|
||||||
import type { AuthState } from "../../utils/AuthService";
|
import type { AuthState } from "../../utils/AuthService";
|
||||||
import LiteElement, { html } from "../../utils/LiteElement";
|
import LiteElement, { html } from "../../utils/LiteElement";
|
||||||
@ -14,6 +20,14 @@ type RunningCrawlsMap = {
|
|||||||
[configId: string]: string;
|
[configId: string]: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MIN_SEARCH_LENGTH = 2;
|
||||||
|
const sortableFieldLabels = {
|
||||||
|
created_desc: msg("Newest"),
|
||||||
|
created_asc: msg("Oldest"),
|
||||||
|
lastCrawlTime_desc: msg("Newest Crawl"),
|
||||||
|
lastCrawlTime_asc: msg("Oldest Crawl"),
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Usage:
|
* Usage:
|
||||||
* ```ts
|
* ```ts
|
||||||
@ -40,9 +54,34 @@ export class CrawlTemplatesList extends LiteElement {
|
|||||||
@state()
|
@state()
|
||||||
selectedTemplateForEdit?: CrawlTemplate;
|
selectedTemplateForEdit?: CrawlTemplate;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private orderBy: {
|
||||||
|
field: "created";
|
||||||
|
direction: "asc" | "desc";
|
||||||
|
} = {
|
||||||
|
field: "created",
|
||||||
|
direction: "desc",
|
||||||
|
};
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private searchBy: string = "";
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private filterByScheduled: boolean | null = null;
|
||||||
|
|
||||||
|
// For fuzzy search:
|
||||||
|
private fuse = new Fuse([], {
|
||||||
|
keys: ["name"],
|
||||||
|
shouldSort: false,
|
||||||
|
threshold: 0.4, // stricter; default is 0.6
|
||||||
|
});
|
||||||
|
|
||||||
async firstUpdated() {
|
async firstUpdated() {
|
||||||
try {
|
try {
|
||||||
this.crawlTemplates = await this.getCrawlTemplates();
|
this.crawlTemplates = await this.getCrawlTemplates();
|
||||||
|
|
||||||
|
// Update search/filter collection
|
||||||
|
this.fuse.setCollection(this.crawlTemplates as any);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.notify({
|
this.notify({
|
||||||
message: msg("Sorry, couldn't retrieve crawl templates at this time."),
|
message: msg("Sorry, couldn't retrieve crawl templates at this time."),
|
||||||
@ -53,43 +92,197 @@ export class CrawlTemplatesList extends LiteElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (!this.crawlTemplates) {
|
return html`
|
||||||
return html`<div
|
<div class="mb-4">${this.renderControls()}</div>
|
||||||
|
|
||||||
|
${this.crawlTemplates
|
||||||
|
? this.crawlTemplates.length
|
||||||
|
? this.renderTemplateList()
|
||||||
|
: html`
|
||||||
|
<div class="border-t border-b py-5">
|
||||||
|
<p class="text-center text-0-500">
|
||||||
|
${msg("No crawl templates yet.")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: html`<div
|
||||||
class="w-full flex items-center justify-center my-24 text-4xl"
|
class="w-full flex items-center justify-center my-24 text-4xl"
|
||||||
>
|
>
|
||||||
<sl-spinner></sl-spinner>
|
<sl-spinner></sl-spinner>
|
||||||
</div>`;
|
</div>`}
|
||||||
|
|
||||||
|
<sl-dialog
|
||||||
|
label=${msg(str`Edit Crawl Schedule`)}
|
||||||
|
?open=${this.showEditDialog}
|
||||||
|
@sl-request-close=${() => (this.showEditDialog = false)}
|
||||||
|
@sl-after-hide=${() => (this.selectedTemplateForEdit = undefined)}
|
||||||
|
>
|
||||||
|
<h2 class="text-lg font-medium mb-4">
|
||||||
|
${this.selectedTemplateForEdit?.name}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
${this.selectedTemplateForEdit
|
||||||
|
? html`
|
||||||
|
<btrix-crawl-scheduler
|
||||||
|
.schedule=${this.selectedTemplateForEdit.schedule}
|
||||||
|
@submit=${this.onSubmitSchedule}
|
||||||
|
></btrix-crawl-scheduler>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
</sl-dialog>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderControls() {
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div class="flex flex-wrap items-center">
|
||||||
class=${this.crawlTemplates.length
|
<div class="grow mr-4 mb-4">
|
||||||
? "grid sm:grid-cols-2 md:grid-cols-3 gap-4 mb-4"
|
<sl-input
|
||||||
: "flex justify-center"}
|
class="w-full"
|
||||||
|
slot="trigger"
|
||||||
|
placeholder=${msg("Search by name")}
|
||||||
|
style="--sl-input-height-medium: 2.25rem;"
|
||||||
|
clearable
|
||||||
|
?disabled=${!this.crawlTemplates?.length}
|
||||||
|
@sl-input=${this.onSearchInput}
|
||||||
>
|
>
|
||||||
|
<sl-icon name="search" slot="prefix"></sl-icon>
|
||||||
|
</sl-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grow-0 mb-4">
|
||||||
<a
|
<a
|
||||||
href=${`/archives/${this.archiveId}/crawl-templates/new`}
|
href=${`/archives/${this.archiveId}/crawl-templates/new`}
|
||||||
class="col-span-1 bg-slate-50 border border-indigo-200 hover:border-primary text-primary text-center font-medium rounded px-6 py-4 transition-colors"
|
class="block bg-indigo-500 hover:bg-indigo-400 text-white text-center font-medium leading-none rounded px-3 py-2 transition-colors"
|
||||||
@click=${this.navLink}
|
|
||||||
role="button"
|
role="button"
|
||||||
|
@click=${this.navLink}
|
||||||
>
|
>
|
||||||
<sl-icon
|
<sl-icon
|
||||||
class="inline-block align-middle mr-2"
|
class="inline-block align-middle mr-2"
|
||||||
name="plus-square"
|
name="plus-lg"
|
||||||
></sl-icon
|
></sl-icon
|
||||||
><span
|
><span class="inline-block align-middle mr-2 text-sm"
|
||||||
class="inline-block align-middle mr-2 ${this.crawlTemplates.length
|
|
||||||
? "text-sm"
|
|
||||||
: "font-medium"}"
|
|
||||||
>${msg("New Crawl Template")}</span
|
>${msg("New Crawl Template")}</span
|
||||||
>
|
>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${this.crawlTemplates && this.crawlTemplates.length
|
||||||
|
? html`<div class="flex flex-wrap items-center justify-between">
|
||||||
|
<div class="text-sm">
|
||||||
|
<button
|
||||||
|
class="inline-block font-medium border-2 border-transparent ${this
|
||||||
|
.filterByScheduled === null
|
||||||
|
? "border-b-current text-primary"
|
||||||
|
: "text-neutral-500"} mr-3"
|
||||||
|
aria-selected=${this.filterByScheduled === null}
|
||||||
|
@click=${() => (this.filterByScheduled = null)}
|
||||||
|
>
|
||||||
|
${msg("All")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="inline-block font-medium border-2 border-transparent ${this
|
||||||
|
.filterByScheduled === true
|
||||||
|
? "border-b-current text-primary"
|
||||||
|
: "text-neutral-500"} mr-3"
|
||||||
|
aria-selected=${this.filterByScheduled === true}
|
||||||
|
@click=${() => (this.filterByScheduled = true)}
|
||||||
|
>
|
||||||
|
${msg("Scheduled")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="inline-block font-medium border-2 border-transparent ${this
|
||||||
|
.filterByScheduled === false
|
||||||
|
? "border-b-current text-primary"
|
||||||
|
: "text-neutral-500"} mr-3"
|
||||||
|
aria-selected=${this.filterByScheduled === false}
|
||||||
|
@click=${() => (this.filterByScheduled = false)}
|
||||||
|
>
|
||||||
|
${msg("No schedule")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-end">
|
||||||
|
<div class="whitespace-nowrap text-sm text-0-500 mr-2">
|
||||||
|
${msg("Sort By")}
|
||||||
|
</div>
|
||||||
|
<sl-dropdown
|
||||||
|
placement="bottom-end"
|
||||||
|
distance="4"
|
||||||
|
@sl-select=${(e: any) => {
|
||||||
|
const [field, direction] = e.detail.item.value.split("_");
|
||||||
|
this.orderBy = {
|
||||||
|
field: field,
|
||||||
|
direction: direction,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<sl-button
|
||||||
|
slot="trigger"
|
||||||
|
size="small"
|
||||||
|
pill
|
||||||
|
caret
|
||||||
|
?disabled=${!this.crawlTemplates?.length}
|
||||||
|
>${(sortableFieldLabels as any)[this.orderBy.field] ||
|
||||||
|
sortableFieldLabels[
|
||||||
|
`${this.orderBy.field}_${this.orderBy.direction}`
|
||||||
|
]}</sl-button
|
||||||
|
>
|
||||||
|
<sl-menu>
|
||||||
|
${Object.entries(sortableFieldLabels).map(
|
||||||
|
([value, label]) => html`
|
||||||
|
<sl-menu-item
|
||||||
|
value=${value}
|
||||||
|
?checked=${value ===
|
||||||
|
`${this.orderBy.field}_${this.orderBy.direction}`}
|
||||||
|
>${label}</sl-menu-item
|
||||||
|
>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</sl-menu>
|
||||||
|
</sl-dropdown>
|
||||||
|
<sl-icon-button
|
||||||
|
name="arrow-down-up"
|
||||||
|
label=${msg("Reverse sort")}
|
||||||
|
@click=${() => {
|
||||||
|
this.orderBy = {
|
||||||
|
...this.orderBy,
|
||||||
|
direction:
|
||||||
|
this.orderBy.direction === "asc" ? "desc" : "asc",
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
></sl-icon-button>
|
||||||
|
</div>
|
||||||
|
</div>`
|
||||||
|
: ""}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderTemplateList() {
|
||||||
|
const flowFns = [
|
||||||
|
orderBy(this.orderBy.field, this.orderBy.direction),
|
||||||
|
map(this.renderTemplateItem.bind(this)),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (this.filterByScheduled === true) {
|
||||||
|
flowFns.unshift(filter(({ schedule }: any) => Boolean(schedule)));
|
||||||
|
} else if (this.filterByScheduled === false) {
|
||||||
|
flowFns.unshift(filter(({ schedule }: any) => !schedule));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.searchBy.length >= MIN_SEARCH_LENGTH) {
|
||||||
|
flowFns.unshift(this.filterResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
<div class="grid sm:grid-cols-2 md:grid-cols-3 gap-4">
|
<div class="grid sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
${this.crawlTemplates.map(
|
${flow(...flowFns)(this.crawlTemplates)}
|
||||||
(t) =>
|
</div>
|
||||||
html`<a
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderTemplateItem(t: CrawlTemplate) {
|
||||||
|
return html`<a
|
||||||
class="block col-span-1 p-1 border shadow hover:shadow-sm hover:bg-zinc-50/50 hover:text-primary rounded text-sm transition-colors"
|
class="block col-span-1 p-1 border shadow hover:shadow-sm hover:bg-zinc-50/50 hover:text-primary rounded text-sm transition-colors"
|
||||||
aria-label=${t.name}
|
aria-label=${t.name}
|
||||||
href=${`/archives/${this.archiveId}/crawl-templates/config/${t.id}`}
|
href=${`/archives/${this.archiveId}/crawl-templates/config/${t.id}`}
|
||||||
@ -111,19 +304,13 @@ export class CrawlTemplatesList extends LiteElement {
|
|||||||
<div class="overflow-hidden">
|
<div class="overflow-hidden">
|
||||||
<sl-tooltip
|
<sl-tooltip
|
||||||
content=${t.config.seeds
|
content=${t.config.seeds
|
||||||
.map((seed) =>
|
.map((seed) => (typeof seed === "string" ? seed : seed.url))
|
||||||
typeof seed === "string" ? seed : seed.url
|
|
||||||
)
|
|
||||||
.join(", ")}
|
.join(", ")}
|
||||||
>
|
>
|
||||||
<div
|
<div class="font-mono whitespace-nowrap truncate text-0-500">
|
||||||
class="font-mono whitespace-nowrap truncate text-0-500"
|
|
||||||
>
|
|
||||||
<span class="underline decoration-dashed"
|
<span class="underline decoration-dashed"
|
||||||
>${t.config.seeds
|
>${t.config.seeds
|
||||||
.map((seed) =>
|
.map((seed) => (typeof seed === "string" ? seed : seed.url))
|
||||||
typeof seed === "string" ? seed : seed.url
|
|
||||||
)
|
|
||||||
.join(", ")}</span
|
.join(", ")}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@ -132,19 +319,14 @@ export class CrawlTemplatesList extends LiteElement {
|
|||||||
<div class="font-mono text-purple-500">
|
<div class="font-mono text-purple-500">
|
||||||
${t.crawlCount === 1
|
${t.crawlCount === 1
|
||||||
? msg(str`${t.crawlCount} crawl`)
|
? msg(str`${t.crawlCount} crawl`)
|
||||||
: msg(
|
: msg(str`${(t.crawlCount || 0).toLocaleString()} crawls`)}
|
||||||
str`${(t.crawlCount || 0).toLocaleString()} crawls`
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
${t.crawlCount
|
${t.crawlCount
|
||||||
? html`<sl-tooltip>
|
? html`<sl-tooltip>
|
||||||
<span slot="content" class="capitalize">
|
<span slot="content" class="capitalize">
|
||||||
${msg(
|
${msg(
|
||||||
str`Last Crawl: ${t.lastCrawlState && t.lastCrawlState.replace(
|
str`Last Crawl: ${t.lastCrawlState && t.lastCrawlState.replace(/_/g, " ")}`
|
||||||
/_/g,
|
|
||||||
" "
|
|
||||||
)}`
|
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<a
|
<a
|
||||||
@ -226,30 +408,7 @@ export class CrawlTemplatesList extends LiteElement {
|
|||||||
</div>
|
</div>
|
||||||
${this.renderCardFooter(t)}
|
${this.renderCardFooter(t)}
|
||||||
</div>
|
</div>
|
||||||
</a>`
|
</a>`;
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<sl-dialog
|
|
||||||
label=${msg(str`Edit Crawl Schedule`)}
|
|
||||||
?open=${this.showEditDialog}
|
|
||||||
@sl-request-close=${() => (this.showEditDialog = false)}
|
|
||||||
@sl-after-hide=${() => (this.selectedTemplateForEdit = undefined)}
|
|
||||||
>
|
|
||||||
<h2 class="text-lg font-medium mb-4">
|
|
||||||
${this.selectedTemplateForEdit?.name}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
${this.selectedTemplateForEdit
|
|
||||||
? html`
|
|
||||||
<btrix-crawl-scheduler
|
|
||||||
.schedule=${this.selectedTemplateForEdit.schedule}
|
|
||||||
@submit=${this.onSubmitSchedule}
|
|
||||||
></btrix-crawl-scheduler>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
</sl-dialog>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderCardMenu(t: CrawlTemplate) {
|
private renderCardMenu(t: CrawlTemplate) {
|
||||||
@ -383,6 +542,16 @@ export class CrawlTemplatesList extends LiteElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onSearchInput = debounce(200)((e: any) => {
|
||||||
|
this.searchBy = e.target.value;
|
||||||
|
}) as any;
|
||||||
|
|
||||||
|
private filterResults = () => {
|
||||||
|
const results = this.fuse.search(this.searchBy);
|
||||||
|
|
||||||
|
return results.map(({ item }) => item);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch crawl templates and record running crawls
|
* Fetch crawl templates and record running crawls
|
||||||
* associated with the crawl templates
|
* associated with the crawl templates
|
||||||
|
@ -129,7 +129,7 @@ export class CrawlsList extends LiteElement {
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<main>
|
<main>
|
||||||
<header class="pb-4">${this.renderControls()}</header>
|
<header class="mb-4">${this.renderControls()}</header>
|
||||||
<section>
|
<section>
|
||||||
${this.crawls.length
|
${this.crawls.length
|
||||||
? this.renderCrawlList()
|
? this.renderCrawlList()
|
||||||
@ -164,10 +164,10 @@ export class CrawlsList extends LiteElement {
|
|||||||
<div class="grid grid-cols-2 gap-3 items-center">
|
<div class="grid grid-cols-2 gap-3 items-center">
|
||||||
<div class="col-span-2 md:col-span-1">
|
<div class="col-span-2 md:col-span-1">
|
||||||
<sl-input
|
<sl-input
|
||||||
|
size="small"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
slot="trigger"
|
slot="trigger"
|
||||||
placeholder=${msg("Search by Crawl Template name or ID")}
|
placeholder=${msg("Search by Crawl Template name or ID")}
|
||||||
pill
|
|
||||||
clearable
|
clearable
|
||||||
?disabled=${!this.crawls?.length}
|
?disabled=${!this.crawls?.length}
|
||||||
@sl-input=${this.onSearchInput}
|
@sl-input=${this.onSearchInput}
|
||||||
@ -177,7 +177,7 @@ export class CrawlsList extends LiteElement {
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-span-12 md:col-span-1 flex items-center justify-end">
|
<div class="col-span-12 md:col-span-1 flex items-center justify-end">
|
||||||
<div class="whitespace-nowrap text-sm text-0-500 mr-2">
|
<div class="whitespace-nowrap text-sm text-0-500 mr-2">
|
||||||
${msg("Sort by")}
|
${msg("Sort By")}
|
||||||
</div>
|
</div>
|
||||||
<sl-dropdown
|
<sl-dropdown
|
||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
@ -192,6 +192,7 @@ export class CrawlsList extends LiteElement {
|
|||||||
>
|
>
|
||||||
<sl-button
|
<sl-button
|
||||||
slot="trigger"
|
slot="trigger"
|
||||||
|
size="small"
|
||||||
pill
|
pill
|
||||||
caret
|
caret
|
||||||
?disabled=${!this.crawls?.length}
|
?disabled=${!this.crawls?.length}
|
||||||
|
Loading…
Reference in New Issue
Block a user