import { state, property, customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { when } from "lit/directives/when.js";
import { msg, localized, str } from "@lit/localize";
import type { AuthState } from "@/utils/AuthService";
import LiteElement, { html } from "@/utils/LiteElement";
import type { Profile } from "./types";
import { isApiError } from "@/utils/api";
import { type SlDropdown } from "@shoelace-style/shoelace";
/**
* Usage:
* ```ts
*
* ```
*/
@localized()
@customElement("btrix-browser-profiles-detail")
export class BrowserProfilesDetail extends LiteElement {
@property({ type: Object })
authState!: AuthState;
@property({ type: String })
orgId!: string;
@property({ type: String })
profileId!: string;
@state()
private profile?: Profile;
@state()
private isBrowserLoading = false;
@state()
private isBrowserLoaded = false;
@state()
private isSubmittingBrowserChange = false;
@state()
private isSubmittingProfileChange = false;
@state()
private browserId?: string;
@state()
private isEditDialogOpen = false;
@state()
private isEditDialogContentVisible = false;
disconnectedCallback() {
if (this.browserId) {
void this.deleteBrowser(this.browserId);
}
}
firstUpdated() {
void this.fetchProfile();
}
render() {
return html`
- ${msg("Description")}
-
${this.profile
? this.profile.description ||
html`${msg("None")}`
: ""}
-
${msg("Created at")}
-
${this.profile
? html`
`
: ""}
-
${msg("Crawl Workflows")}
-
${this.profile?.crawlconfigs?.map(
({ id, name }) => html`
-
${name}
`,
)}
${msg("Browser Profile")}
${when(
this.browserId || this.isBrowserLoading,
() => html`
(this.isBrowserLoaded = true)}
>
${this.renderBrowserProfileControls()}
`,
() =>
html`
${msg(
"Edit the profile to make changes or view its present configuration",
)}
${msg(
"Edit Browser Profile",
)}
`,
)}
${when(
!(this.browserId || this.isBrowserLoading),
this.renderVisitedSites,
)}
(this.isEditDialogOpen = false)}
@sl-show=${() => (this.isEditDialogContentVisible = true)}
@sl-after-hide=${() => (this.isEditDialogContentVisible = false)}
>
${this.isEditDialogContentVisible ? this.renderEditProfile() : ""}
`;
}
private readonly renderVisitedSites = () => {
return html`
${this.profile?.origins.map((origin) => html`- ${origin}
`)}
`;
};
private renderBrowserProfileControls() {
return html`
${msg(
"Interact with the browsing tool to set up the browser profile. Workflows that use this browser profile will behave as if they have logged into the same websites and have the same cookies that have been set here.",
)}
${msg("Cancel")}
${msg("Save Browser Profile")}
`;
}
private renderMenu() {
return html`
${msg("Actions")}
- {
void (e.target as HTMLElement)
.closest("sl-dropdown")!
.hide();
this.isEditDialogOpen = true;
}}
>
${msg("Edit name & description")}
- void this.duplicateProfile()}
>
${msg("Duplicate profile")}
- {
void this.deleteProfile();
}}
>
${msg("Delete")}
`;
}
private renderEditProfile() {
if (!this.profile) return;
return html`
`;
}
private async startBrowserPreview() {
if (!this.profile) return;
this.isBrowserLoading = true;
const url = this.profile.origins[0];
try {
const data = await this.createBrowser({ url });
this.browserId = data.browserid;
this.isBrowserLoading = false;
} catch (e) {
this.isBrowserLoading = false;
this.notify({
message: msg("Sorry, couldn't preview browser profile at this time."),
variant: "danger",
icon: "exclamation-octagon",
});
}
}
private async cancelEditBrowser() {
const prevBrowserId = this.browserId;
this.isBrowserLoaded = false;
this.browserId = undefined;
if (prevBrowserId) {
try {
await this.deleteBrowser(prevBrowserId);
} catch (e) {
// TODO Investigate DELETE is returning 404
console.debug(e);
}
}
}
private async duplicateProfile() {
if (!this.profile) return;
this.isBrowserLoading = true;
const url = this.profile.origins[0];
try {
const data = await this.createBrowser({ url });
this.notify({
message: msg("Starting up browser with current profile..."),
variant: "success",
icon: "check2-circle",
});
this.navTo(
`${this.orgBasePath}/browser-profiles/profile/browser/${
data.browserid
}?name=${window.encodeURIComponent(
this.profile.name,
)}&description=${window.encodeURIComponent(
this.profile.description || "",
)}&profileId=${window.encodeURIComponent(this.profile.id)}&navigateUrl=`,
);
} catch (e) {
this.isBrowserLoading = false;
this.notify({
message: msg("Sorry, couldn't create browser profile at this time."),
variant: "danger",
icon: "exclamation-octagon",
});
}
}
private async deleteProfile() {
const profileName = this.profile!.name;
try {
const data = await this.apiFetch(
`/orgs/${this.orgId}/profiles/${this.profile!.id}`,
this.authState!,
{
method: "DELETE",
},
);
if (data.error && data.crawlconfigs) {
this.notify({
message: msg(
html`Could not delete ${profileName}, in use by
${data.crawlconfigs.map(({ name }) => name).join(", ")}. Please remove browser profile from Workflow to continue.`,
),
variant: "warning",
icon: "exclamation-triangle",
duration: 15000,
});
} else {
this.navTo(`${this.orgBasePath}/browser-profiles`);
this.notify({
message: msg(html`Deleted ${profileName}.`),
variant: "success",
icon: "check2-circle",
});
}
} 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,
profileId: this.profile!.id,
};
return this.apiFetch<{ browserid: string }>(
`/orgs/${this.orgId}/profiles/browser`,
this.authState!,
{
method: "POST",
body: JSON.stringify(params),
},
);
}
private async deleteBrowser(id: string) {
return this.apiFetch(
`/orgs/${this.orgId}/profiles/browser/${id}`,
this.authState!,
{
method: "DELETE",
},
);
}
/**
* Fetch browser profile and update internal state
*/
private async fetchProfile(): Promise {
try {
const data = await this.getProfile();
this.profile = data;
} catch (e) {
this.notify({
message: msg("Sorry, couldn't retrieve browser profiles at this time."),
variant: "danger",
icon: "exclamation-octagon",
});
}
}
private async getProfile() {
const data = await this.apiFetch(
`/orgs/${this.orgId}/profiles/${this.profileId}`,
this.authState!,
);
return data;
}
private async saveBrowser() {
if (!this.browserId) return;
if (
!window.confirm(
msg(
"Save browser changes to profile? You will need to re-load the browsing tool to make additional changes.",
),
)
) {
return;
}
this.isSubmittingBrowserChange = true;
const params = {
name: this.profile!.name,
browserid: this.browserId,
};
try {
const data = await this.apiFetch<{ updated: boolean }>(
`/orgs/${this.orgId}/profiles/${this.profileId}`,
this.authState!,
{
method: "PATCH",
body: JSON.stringify(params),
},
);
if (data.updated) {
this.notify({
message: msg("Successfully saved browser profile."),
variant: "success",
icon: "check2-circle",
});
this.browserId = undefined;
} else {
throw data;
}
} catch (e) {
this.notify({
message: msg("Sorry, couldn't save browser profile at this time."),
variant: "danger",
icon: "exclamation-octagon",
});
}
this.isSubmittingBrowserChange = false;
}
private async onSubmitEdit(e: SubmitEvent) {
e.preventDefault;
this.isSubmittingProfileChange = true;
const formData = new FormData(e.target as HTMLFormElement);
const name = formData.get("name") as string;
const description = formData.get("description") as string;
const params = {
name,
description,
};
try {
const data = await this.apiFetch<{ updated: boolean }>(
`/orgs/${this.orgId}/profiles/${this.profileId}`,
this.authState!,
{
method: "PATCH",
body: JSON.stringify(params),
},
);
if (data.updated) {
this.notify({
message: msg("Successfully saved browser profile."),
variant: "success",
icon: "check2-circle",
});
this.profile = {
...this.profile,
...params,
} as Profile;
this.isEditDialogOpen = false;
} else {
throw data;
}
} catch (e) {
let message = msg("Sorry, couldn't save browser profile at this time.");
if (isApiError(e) && e.statusCode === 403) {
if (e.details === "storage_quota_reached") {
message = msg(
"Your org does not have enough storage to save this browser profile.",
);
} else {
message = msg("You do not have permission to edit browser profiles.");
}
}
this.notify({
message: message,
variant: "danger",
icon: "exclamation-octagon",
});
}
this.isSubmittingProfileChange = false;
}
/**
* Stop propgation of sl-select events.
* Prevents bug where sl-dialog closes when dropdown closes
* https://github.com/shoelace-style/shoelace/issues/170
*/
private stopProp(e: CustomEvent) {
e.stopPropagation();
}
}