diff --git a/frontend/src/index.ts b/frontend/src/index.ts index edc3066e..45a152e3 100644 --- a/frontend/src/index.ts +++ b/frontend/src/index.ts @@ -431,6 +431,7 @@ export class App extends LiteElement { case "archiveNewResourceTab": case "archiveCrawl": case "browserProfile": + case "browser": case "crawlTemplate": case "crawlTemplateEdit": return html` + * * ``` */ @localized() @@ -17,20 +20,74 @@ export class BrowserProfilesList extends LiteElement { authState!: AuthState; @property({ type: String }) - archiveId?: string; + archiveId!: string; + + @property({ type: Boolean }) + showCreateDialog = false; @state() browserProfiles?: Profile[]; + @state() + private isCreateFormVisible = false; + + @state() + private isSubmitting = false; + + /** Profile creation only works in Chromium-based browsers */ + private isBrowserCompatible = Boolean((window as any).chrome); + firstUpdated() { - this.fetchCrawls(); + if (this.showCreateDialog) { + this.isCreateFormVisible = true; + } + + this.fetchBrowserProfiles(); } render() { - return html` ${this.renderTable()} `; + return html`
+ + ${msg("New Browser Profile")} + +
+ + ${this.renderTable()} + + (this.isCreateFormVisible = true)} + @sl-after-hide=${() => (this.isCreateFormVisible = false)} + > + ${this.isBrowserCompatible + ? "" + : html` +
+ + ${msg( + "Browser profile creation is only supported in Chromium-based browsers (such as Chrome) at this time. Please re-open this page in a compatible browser to proceed." + )} + +
+ `} + ${this.isCreateFormVisible ? this.renderCreateForm() : ""} +
`; } - renderTable() { + private renderTable() { return html`
@@ -90,10 +147,132 @@ export class BrowserProfilesList extends LiteElement { `; } + private renderCreateForm() { + return html` +
+
+ + +
+ + http:// + https:// + + + +
+
+ +
+ + ${msg("More options")} + + +
+ + ${this.browserProfiles?.map( + (profile) => html` + ${profile.name} + ` + )} + +
+
+ +
+ ${msg("Cancel")} + + ${msg("Start Browser")} + +
+
+
`; + } + + private hideDialog() { + this.navTo(`/archives/${this.archiveId}/browser-profiles`); + } + + async onSubmit(event: { detail: { formData: FormData } }) { + this.isSubmitting = true; + + const { formData } = event.detail; + const url = formData.get("url") as string; + const params = { + url: `${formData.get("urlPrefix")}${url.substring(url.indexOf(",") + 1)}`, + baseId: formData.get("baseId"), + }; + + try { + const data = await this.apiFetch( + `/archives/${this.archiveId}/profiles/browser`, + this.authState!, + { + method: "POST", + body: JSON.stringify(params), + } + ); + + this.notify({ + message: msg("Starting up browser for profile creation."), + type: "success", + icon: "check2-circle", + }); + + this.navTo( + `/archives/${this.archiveId}/browser-profiles/profile/browser/${data.browserid}` + ); + } catch (e) { + this.isSubmitting = false; + + this.notify({ + message: msg("Sorry, couldn't create browser profile at this time."), + type: "danger", + icon: "exclamation-octagon", + }); + } + } + /** * Fetch browser profiles and update internal state */ - private async fetchCrawls(): Promise { + private async fetchBrowserProfiles(): Promise { try { const data = await this.getProfiles(); @@ -108,10 +287,6 @@ export class BrowserProfilesList extends LiteElement { } private async getProfiles(): Promise { - if (!this.archiveId) { - throw new Error(`Archive ID ${typeof this.archiveId}`); - } - const data = await this.apiFetch( `/archives/${this.archiveId}/profiles`, this.authState! @@ -119,6 +294,15 @@ export class BrowserProfilesList extends LiteElement { return data; } + + /** + * 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(); + } } customElements.define("btrix-browser-profiles-list", BrowserProfilesList); diff --git a/frontend/src/pages/archive/browser-profiles-new.ts b/frontend/src/pages/archive/browser-profiles-new.ts new file mode 100644 index 00000000..63ec948a --- /dev/null +++ b/frontend/src/pages/archive/browser-profiles-new.ts @@ -0,0 +1,337 @@ +import { state, property } from "lit/decorators.js"; +import { msg, localized, str } from "@lit/localize"; +import { ref } from "lit/directives/ref.js"; + +import type { AuthState } from "../../utils/AuthService"; +import LiteElement, { html } from "../../utils/LiteElement"; + +/** + * Usage: + * ```ts + * + * ``` + */ +@localized() +export class BrowserProfilesNew extends LiteElement { + @property({ type: Object }) + authState!: AuthState; + + @property({ type: String }) + archiveId!: string; + + @property({ type: String }) + browserId!: string; + + @state() + private browserUrl?: string; + + @state() + private isSubmitting = false; + + @state() + private hasFetchError = false; + + @state() + private isFullscreen = false; + + private pollTimerId?: number; + + connectedCallback() { + super.connectedCallback(); + + document.addEventListener("fullscreenchange", this.onFullscreenChange); + } + + disconnectedCallback() { + window.clearTimeout(this.pollTimerId); + document.removeEventListener("fullscreenchange", this.onFullscreenChange); + } + + firstUpdated() { + this.fetchBrowser(); + } + + render() { + return html` +
+

+ ${msg( + "Interact with the browser to record your browser profile. When you’re finished interacting, name and save the profile." + )} +

+
+ +
+ ${this.hasFetchError + ? html` + + ${msg( + html`The interactive browser is not available. Try creating a + new browser profile. + Create New` + )} + + ` + : html` +
+
+ ${this.browserUrl + ? this.renderBrowser() + : html` +
+ +
+ `} +
+
+ ${document.fullscreenEnabled + ? html` +
+ + this.isFullscreen + ? document.exitFullscreen() + : this.enterFullscreen("interactive-browser")} + > + ${this.isFullscreen + ? html` + + ${msg("Exit")} + ` + : html` + + ${msg("Go Fullscreen")} + `} + +
+ ` + : ""} + +
${this.renderForm()}
+
+
+ `} +
+ `; + } + + private renderForm() { + return html` +
+ + + + +
+ + ${msg("Save Profile")} + +
+
+
`; + } + + private renderBrowser() { + return html` + + `; + } + + /** + * Fetch browser profiles and update internal state + */ + private async fetchBrowser(): Promise { + try { + await this.checkBrowserStatus(); + } catch (e) { + this.hasFetchError = true; + + this.notify({ + message: msg("Sorry, couldn't create browser profile at this time."), + type: "danger", + icon: "exclamation-octagon", + }); + } + } + + /** + * Check whether temporary browser is up + **/ + private async checkBrowserStatus() { + const result = await this.getBrowser(); + + if (result.detail === "waiting_for_browser") { + this.pollTimerId = window.setTimeout( + () => this.checkBrowserStatus(), + 5 * 1000 + ); + } else if (result.url) { + this.browserUrl = result.url; + + this.pingBrowser(); + } else { + console.debug("Unknown checkBrowserStatus state"); + } + } + + private async getBrowser(): Promise<{ + detail?: string; + url?: string; + }> { + const data = await this.apiFetch( + `/archives/${this.archiveId}/profiles/browser/${this.browserId}`, + this.authState! + ); + + return data; + } + + /** + * Ping temporary browser every minute to keep it alive + **/ + private async pingBrowser() { + await this.apiFetch( + `/archives/${this.archiveId}/profiles/browser/${this.browserId}/ping`, + this.authState!, + { + method: "POST", + } + ); + + this.pollTimerId = window.setTimeout(() => this.pingBrowser(), 60 * 1000); + } + + private async onSubmit(event: { detail: { formData: FormData } }) { + this.isSubmitting = true; + + if (this.isFullscreen) { + await document.exitFullscreen(); + } + + const { formData } = event.detail; + const params = { + name: formData.get("name"), + description: formData.get("description"), + }; + + try { + const data = await this.apiFetch( + `/archives/${this.archiveId}/profiles/browser/${this.browserId}/commit`, + this.authState!, + { + method: "POST", + body: JSON.stringify(params), + } + ); + + this.notify({ + message: msg("Successfully created browser profile."), + type: "success", + icon: "check2-circle", + }); + + // TODO nav to detail page + // this.navTo( + // `/archives/${this.archiveId}/browser-profiles/profile/${data.id}` + // ); + this.navTo(`/archives/${this.archiveId}/browser-profiles`); + } catch (e) { + this.isSubmitting = false; + + this.notify({ + message: msg("Sorry, couldn't create browser profile at this time."), + type: "danger", + icon: "exclamation-octagon", + }); + } + } + + private onIframeRef(el: HTMLIFrameElement) { + el.addEventListener("load", () => { + // TODO see if we can make this work locally without CORs errors + el.contentWindow?.localStorage.setItem("uiTheme", '"default"'); + el.contentWindow?.localStorage.setItem( + "InspectorView.screencastSplitViewState", + '{"vertical":{"size":241}}' + ); + }); + } + + /** + * Enter fullscreen mode + * @param id ID of element to fullscreen + */ + private async enterFullscreen(id: string) { + try { + document.getElementById(id)!.requestFullscreen({ + // Show browser navigation controls + navigationUI: "show", + }); + } catch (err) { + console.error(err); + } + } + + private onFullscreenChange = () => { + if (document.fullscreenElement) { + this.isFullscreen = true; + } else { + this.isFullscreen = false; + } + }; +} + +customElements.define("btrix-browser-profiles-new", BrowserProfilesNew); diff --git a/frontend/src/pages/archive/crawl-templates-list.ts b/frontend/src/pages/archive/crawl-templates-list.ts index 11591156..1b2db067 100644 --- a/frontend/src/pages/archive/crawl-templates-list.ts +++ b/frontend/src/pages/archive/crawl-templates-list.ts @@ -69,7 +69,7 @@ export class CrawlTemplatesList extends LiteElement { > @@ -81,7 +81,7 @@ export class CrawlTemplatesList extends LiteElement { class="inline-block align-middle mr-2 ${this.crawlTemplates.length ? "text-sm" : "font-medium"}" - >${msg("Create New Crawl Template")}${msg("New Crawl Template")}
diff --git a/frontend/src/pages/archive/crawl-templates-new.ts b/frontend/src/pages/archive/crawl-templates-new.ts index 08596e8a..6a36cbbe 100644 --- a/frontend/src/pages/archive/crawl-templates-new.ts +++ b/frontend/src/pages/archive/crawl-templates-new.ts @@ -156,8 +156,8 @@ export class CrawlTemplatesNew extends LiteElement { -

${msg("New Crawl Template")}

-

+

${msg("New Crawl Template")}

+

${msg( "Configure a new crawl template. You can choose to run a crawl immediately upon saving this template." )} @@ -225,9 +225,7 @@ export class CrawlTemplatesNew extends LiteElement { `; } + if (this.browserId) { + return html``; + } + return html``; } diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index d0490039..93bcd3be 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -16,6 +16,7 @@ export const ROUTES = { archiveAddMember: "/archives/:id/:tab/add-member", archiveCrawl: "/archives/:id/:tab/crawl/:crawlId", browserProfile: "/archives/:id/:tab/profile/:browserProfileId", + browser: "/archives/:id/:tab/profile/browser/:browserId", crawlTemplate: "/archives/:id/:tab/config/:crawlConfigId", crawlTemplateEdit: "/archives/:id/:tab/config/:crawlConfigId?edit", users: "/users",