browsertrix/frontend/src/components/exclusion-editor.ts
sua yoo b9a24fa5e2
Combine watch crawl with crawl queue (#710)
- crawl queue and watch page are now part of single view
- exclusions can be edited via 'Edit Exclusions' popup
2023-03-17 21:04:08 -07:00

263 lines
6.4 KiB
TypeScript

import { property, state } from "lit/decorators.js";
import { msg, localized, str } from "@lit/localize";
import type { ExclusionRemoveEvent } from "./queue-exclusion-table";
import type {
ExclusionAddEvent,
ExclusionChangeEvent,
} from "./queue-exclusion-form";
import type { SeedConfig } from "../pages/org/types";
import LiteElement, { html } from "../utils/LiteElement";
import type { AuthState } from "../utils/AuthService";
type URLs = string[];
type ResponseData = {
total: number;
matched: URLs;
};
/**
* Crawl queue exclusion editor
*
* Usage example:
* ```ts
* <btrix-exclusion-editor
* orgId=${this.crawl.oid}
* crawlId=${this.crawl.id}
* .config=${this.workflow.config}
* .authState=${this.authState}
* ?isActiveCrawl=${isActive}
* >
* </btrix-exclusion-editor>
* ```
*
* @event on-success On successful edit
*/
@localized()
export class ExclusionEditor extends LiteElement {
@property({ type: Object })
authState?: AuthState;
@property({ type: String })
orgId?: string;
@property({ type: String })
crawlId?: string;
@property({ type: Array })
config?: SeedConfig;
@property({ type: Boolean })
isActiveCrawl = false;
@state()
private isSubmitting = false;
@state()
private exclusionFieldErrorMessage = "";
@state()
/** `new RegExp` constructor string */
private regex: string = "";
@state()
matchedURLs: URLs | null = null;
@state()
private isLoading = false;
willUpdate(changedProperties: Map<string, any>) {
if (
changedProperties.has("authState") ||
changedProperties.has("orgId") ||
changedProperties.has("crawlId") ||
changedProperties.has("regex")
) {
this.fetchQueueMatches();
}
}
render() {
return html`
<div class="grid gap-6 grid-cols-1 lg:grid-cols-2">
<div class="col-span-1">${this.renderTable()}</div>
<div class="col-span-1">
${this.isActiveCrawl && this.regex
? html` <section class="mt-5">${this.renderPending()}</section> `
: ""}
${this.isActiveCrawl
? html` <section class="mt-5">${this.renderQueue()}</section> `
: ""}
</div>
</div>
`;
}
private renderTable() {
return html`
${this.config
? html`<btrix-queue-exclusion-table
?removable=${this.isActiveCrawl}
.exclusions=${this.config.exclude || []}
@on-remove=${this.deleteExclusion}
>
</btrix-queue-exclusion-table>`
: html`
<div class="flex items-center justify-center my-9 text-xl">
<sl-spinner></sl-spinner>
</div>
`}
${this.isActiveCrawl
? html`<div class="mt-2">
<btrix-queue-exclusion-form
?isSubmitting=${this.isSubmitting}
fieldErrorMessage=${this.exclusionFieldErrorMessage}
@on-change=${this.handleRegexChange}
@on-add=${this.handleAddRegex}
>
</btrix-queue-exclusion-form>
</div>`
: ""}
`;
}
private renderPending() {
return html`
<btrix-crawl-pending-exclusions
.matchedURLs=${this.matchedURLs}
></btrix-crawl-pending-exclusions>
`;
}
private renderQueue() {
return html`<btrix-crawl-queue
orgId=${this.orgId!}
crawlId=${this.crawlId!}
.authState=${this.authState}
regex=${this.regex}
matchedTotal=${this.matchedURLs?.length || 0}
></btrix-crawl-queue>`;
}
private handleRegexChange(e: ExclusionChangeEvent) {
const { value, valid } = e.detail;
if (valid) {
this.regex = value;
} else {
this.regex = "";
}
}
private async deleteExclusion(e: ExclusionRemoveEvent) {
const { regex } = e.detail;
try {
const data = await this.apiFetch(
`/orgs/${this.orgId}/crawls/${this.crawlId}/exclusions?regex=${regex}`,
this.authState!,
{
method: "DELETE",
}
);
if (data.success) {
this.notify({
message: msg(html`Removed exclusion: <code>${regex}</code>`),
variant: "success",
icon: "check2-circle",
});
this.dispatchEvent(new CustomEvent("on-success"));
} else {
throw data;
}
} catch (e: any) {
this.notify({
message:
e.message === "crawl_running_cant_deactivate"
? msg("Cannot remove exclusion when crawl is no longer running.")
: msg("Sorry, couldn't remove exclusion at this time."),
variant: "danger",
icon: "exclamation-octagon",
});
}
}
private async fetchQueueMatches() {
if (!this.regex) {
this.matchedURLs = null;
return;
}
this.isLoading = true;
try {
const { matched } = await this.getQueueMatches();
this.matchedURLs = matched;
} catch (e) {
this.notify({
message: msg("Sorry, couldn't fetch pending exclusions at this time."),
variant: "danger",
icon: "exclamation-octagon",
});
}
this.isLoading = false;
}
private async getQueueMatches(): Promise<ResponseData> {
const data: ResponseData = await this.apiFetch(
`/orgs/${this.orgId}/crawls/${this.crawlId}/queueMatchAll?regex=${this.regex}`,
this.authState!
);
return data;
}
private async handleAddRegex(e: ExclusionAddEvent) {
this.isSubmitting = true;
const { regex, onSuccess } = e.detail;
try {
const data = await this.apiFetch(
`/orgs/${this.orgId}/crawls/${this.crawlId}/exclusions?regex=${regex}`,
this.authState!,
{
method: "POST",
}
);
if (data.success) {
this.notify({
message: msg("Exclusion added."),
variant: "success",
icon: "check2-circle",
});
this.regex = "";
this.matchedURLs = null;
await this.updateComplete;
onSuccess();
this.dispatchEvent(new CustomEvent("on-success"));
} else {
throw data;
}
} catch (e: any) {
if (e.message === "exclusion_already_exists") {
this.exclusionFieldErrorMessage = msg("Exclusion already exists");
} else {
this.notify({
message: msg("Sorry, couldn't add exclusion at this time."),
variant: "danger",
icon: "exclamation-octagon",
});
}
}
this.isSubmitting = false;
}
}