browsertrix/frontend/src/pages/org/browser-profiles-list.ts
Emma Segal-Grossman d1156b0145
enable a few more useful eslint suggestions & correct some more types (#1517)
## Changes

Implements suggestions from
https://typescript-eslint.io/blog/consistent-type-imports-and-exports-why-and-how/
and
https://www.totaltypescript.com/method-shorthand-syntax-considered-harmful,
along with a couple more auto-fixable consistency rules.

Of note:
- Functions that return a promise are marked as async
- Suggestions now appear for where to simplify boolean checks,
non-nullish assertions, and optional chaining
2024-02-09 16:14:08 -08:00

296 lines
8.8 KiB
TypeScript

import { state, property, customElement } from "lit/decorators.js";
import { msg, localized } from "@lit/localize";
import type { AuthState } from "@/utils/AuthService";
import LiteElement, { html } from "@/utils/LiteElement";
import type { Profile } from "./types";
import type { APIPaginatedList } from "@/types/api";
import type { SelectNewDialogEvent } from "./index";
import type { Browser } from "@/types/browser";
import { nothing } from "lit";
/**
* Usage:
* ```ts
* <btrix-browser-profiles-list
* authState=${authState}
* orgId=${orgId}
* ></btrix-browser-profiles-list>
* ```
*/
@localized()
@customElement("btrix-browser-profiles-list")
export class BrowserProfilesList extends LiteElement {
@property({ type: Object })
authState!: AuthState;
@property({ type: String })
orgId!: string;
@state()
browserProfiles?: Profile[];
firstUpdated() {
void this.fetchBrowserProfiles();
}
render() {
return html`<header>
<div class="mb-4 flex h-8 w-full justify-between">
<h1 class="text-xl font-semibold">${msg("Browser Profiles")}</h1>
<sl-button
variant="primary"
size="small"
@click=${() => {
this.dispatchEvent(
new CustomEvent("select-new-dialog", {
detail: "browser-profile",
}) as SelectNewDialogEvent,
);
}}
>
<sl-icon slot="prefix" name="plus-lg"></sl-icon>
${msg("New Browser Profile")}
</sl-button>
</div>
</header>
<div class="overflow-auto px-2 pb-1">${this.renderTable()}</div>`;
}
private renderTable() {
return html`
<btrix-table
style="grid-template-columns: min-content [clickable-start] 60ch repeat(2, auto) [clickable-end] min-content; --btrix-cell-padding-left: var(--sl-spacing-x-small); --btrix-cell-padding-right: var(--sl-spacing-x-small);"
>
<btrix-table-head class="mb-2">
<btrix-table-header-cell>
<span class="sr-only">${msg("Backed up status")}</span>
</btrix-table-header-cell>
<btrix-table-header-cell class="pl-0">
${msg("Name")}
</btrix-table-header-cell>
<btrix-table-header-cell>
${msg("Date Created")}
</btrix-table-header-cell>
<btrix-table-header-cell>
${msg("Visited URLs")}
</btrix-table-header-cell>
<btrix-table-header-cell>
<span class="sr-only">${msg("Row Actions")}</span>
</btrix-table-header-cell>
</btrix-table-head>
${this.browserProfiles?.length
? html`
<btrix-table-body
style="--btrix-row-gap: var(--sl-spacing-x-small); --btrix-cell-padding-top: var(--sl-spacing-2x-small); --btrix-cell-padding-bottom: var(--sl-spacing-2x-small);"
>
${this.browserProfiles.map(this.renderItem)}
</btrix-table-body>
`
: nothing}
</btrix-table>
${this.browserProfiles?.length
? nothing
: html`
<div class="border-b border-t py-5">
<p class="text-center text-0-500">
${msg("No browser profiles yet.")}
</p>
</div>
`}
`;
}
private readonly renderItem = (data: Profile) => {
const isBackedUp = data.resource && data.resource.replicas.length > 0;
return html`
<btrix-table-row
class="cursor-pointer select-none rounded border shadow transition-all focus-within:bg-neutral-50 hover:bg-neutral-50 hover:shadow-none"
>
<btrix-table-cell class="p-3">
<sl-tooltip
content=${isBackedUp ? msg("Backed up") : msg("Not backed up")}
>
<sl-icon
name=${isBackedUp ? "clouds" : "cloud-slash"}
class="${isBackedUp ? "text-success" : "text-neutral-500"}"
></sl-icon>
</sl-tooltip>
</btrix-table-cell>
<btrix-table-cell
class="flex-col items-start justify-center pl-0"
rowClickTarget="a"
>
<a
class="flex items-center gap-3"
href=${`${this.orgBasePath}/browser-profiles/profile/${data.id}`}
@click=${this.navLink}
>
${data.name}
</a>
<div class="w-full text-xs text-neutral-500">
<div class="truncate">
${data.description} ${data.description} ${data.description}
${data.description} ${data.description} ${data.description}
${data.description}
</div>
</div>
</btrix-table-cell>
<btrix-table-cell class="whitespace-nowrap">
<sl-format-date
date=${`${data.created}Z`}
month="2-digit"
day="2-digit"
year="2-digit"
hour="2-digit"
minute="2-digit"
></sl-format-date>
</btrix-table-cell>
<btrix-table-cell>${data.origins.join(", ")}</btrix-table-cell>
<btrix-table-cell class="px-1"
>${this.renderActions(data)}</btrix-table-cell
>
</btrix-table-row>
`;
};
private renderActions(data: Profile) {
return html`
<btrix-overflow-dropdown @click=${(e: Event) => e.preventDefault()}>
<sl-menu>
<sl-menu-item
@click=${() => {
void this.duplicateProfile(data);
}}
>
<sl-icon slot="prefix" name="files"></sl-icon>
${msg("Duplicate profile")}
</sl-menu-item>
<sl-menu-item
style="--sl-color-neutral-700: var(--danger)"
@click=${() => {
void this.deleteProfile(data);
}}
>
<sl-icon slot="prefix" name="trash3"></sl-icon>
${msg("Delete")}
</sl-menu-item>
</sl-menu>
</btrix-overflow-dropdown>
`;
}
private async duplicateProfile(profile: Profile) {
const url = profile.origins[0];
try {
const data = await this.createBrowser({ url });
this.notify({
message: msg("Starting up browser with selected profile..."),
variant: "success",
icon: "check2-circle",
});
this.navTo(
`${this.orgBasePath}/browser-profiles/profile/browser/${
data.browserid
}?name=${window.encodeURIComponent(
profile.name,
)}&description=${window.encodeURIComponent(
profile.description || "",
)}&profileId=${window.encodeURIComponent(profile.id)}&navigateUrl=`,
);
} catch (e) {
this.notify({
message: msg("Sorry, couldn't create browser profile at this time."),
variant: "danger",
icon: "exclamation-octagon",
});
}
}
private async deleteProfile(profile: Profile) {
try {
const data = await this.apiFetch<Profile & { error?: boolean }>(
`/orgs/${this.orgId}/profiles/${profile.id}`,
this.authState!,
{
method: "DELETE",
},
);
if (data.error && data.crawlconfigs) {
this.notify({
message: msg(
html`Could not delete <strong>${profile.name}</strong>, in use by
<strong
>${data.crawlconfigs.map(({ name }) => name).join(", ")}</strong
>. Please remove browser profile from Workflow to continue.`,
),
variant: "warning",
icon: "exclamation-triangle",
duration: 15000,
});
} else {
this.notify({
message: msg(html`Deleted <strong>${profile.name}</strong>.`),
variant: "success",
icon: "check2-circle",
});
this.browserProfiles = this.browserProfiles!.filter(
(p) => p.id !== profile.id,
);
}
} catch (e) {
this.notify({
message: msg("Sorry, couldn't delete browser profile at this time."),
variant: "danger",
icon: "exclamation-octagon",
});
}
}
private async createBrowser({ url }: { url: string }) {
const params = {
url,
};
return this.apiFetch<Browser>(
`/orgs/${this.orgId}/profiles/browser`,
this.authState!,
{
method: "POST",
body: JSON.stringify(params),
},
);
}
/**
* Fetch browser profiles and update internal state
*/
private async fetchBrowserProfiles(): Promise<void> {
try {
const data = await this.getProfiles();
this.browserProfiles = data;
} catch (e) {
this.notify({
message: msg("Sorry, couldn't retrieve browser profiles at this time."),
variant: "danger",
icon: "exclamation-octagon",
});
}
}
private async getProfiles() {
const data = await this.apiFetch<APIPaginatedList<Profile>>(
`/orgs/${this.orgId}/profiles`,
this.authState!,
);
return data.items;
}
}