Filter and sort crawl templates (#217)

This commit is contained in:
sua yoo 2022-04-23 20:11:53 -07:00 committed by GitHub
parent cb80c6767e
commit f157e2031f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 347 additions and 177 deletions

View File

@ -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}
> >

View File

@ -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

View File

@ -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}