From 0a6583a9f55885b6a6189d25964b951724a41605 Mon Sep 17 00:00:00 2001 From: sua yoo Date: Tue, 4 Nov 2025 22:11:07 -0800 Subject: [PATCH 01/27] wip new page --- frontend/src/components/ui/card.ts | 12 +- .../components/ui/table/table-header-cell.ts | 3 + frontend/src/controllers/localize.ts | 5 +- .../src/features/archived-items/crawl-logs.ts | 5 +- frontend/src/layouts/emptyMessage.ts | 6 +- frontend/src/layouts/page.ts | 8 +- frontend/src/layouts/panel.ts | 23 +- frontend/src/layouts/secondaryPanel.ts | 45 ++ .../src/pages/org/browser-profiles/index.ts | 1 + .../src/pages/org/browser-profiles/profile.ts | 479 ++++++++++++++++++ frontend/src/pages/org/collections-list.ts | 2 + frontend/src/pages/org/dashboard.ts | 8 +- frontend/src/pages/org/index.ts | 7 +- frontend/src/theme.stylesheet.css | 6 + frontend/src/utils/pluralize.ts | 26 + 15 files changed, 609 insertions(+), 27 deletions(-) create mode 100644 frontend/src/layouts/secondaryPanel.ts create mode 100644 frontend/src/pages/org/browser-profiles/index.ts create mode 100644 frontend/src/pages/org/browser-profiles/profile.ts diff --git a/frontend/src/components/ui/card.ts b/frontend/src/components/ui/card.ts index d94e6ee79b..492832a37f 100644 --- a/frontend/src/components/ui/card.ts +++ b/frontend/src/components/ui/card.ts @@ -2,18 +2,18 @@ import { html } from "lit"; import { customElement } from "lit/decorators.js"; import { TailwindElement } from "@/classes/TailwindElement"; +import { secondaryHeading } from "@/layouts/secondaryPanel"; @customElement("btrix-card") export class Card extends TailwindElement { render() { return html`
-
- -
+ ${secondaryHeading({ + id: "cardHeading", + heading: html``, + })} +
diff --git a/frontend/src/components/ui/table/table-header-cell.ts b/frontend/src/components/ui/table/table-header-cell.ts index d522af2340..0c14ae7687 100644 --- a/frontend/src/components/ui/table/table-header-cell.ts +++ b/frontend/src/components/ui/table/table-header-cell.ts @@ -16,6 +16,9 @@ export class TableHeaderCell extends TableCell { @property({ type: String, reflect: true, noAccessor: true }) role = "columnheader"; + @property({ type: String, reflect: true, noAccessor: true }) + scope?: "row" | "col"; + @property({ type: String, reflect: true }) ariaSort: SortValues = "none"; diff --git a/frontend/src/controllers/localize.ts b/frontend/src/controllers/localize.ts index 0690580e56..328d780a14 100644 --- a/frontend/src/controllers/localize.ts +++ b/frontend/src/controllers/localize.ts @@ -37,9 +37,10 @@ export class LocalizeController extends SlLocalizeController { year: "numeric", month: "long", day: "numeric", - hour: "2-digit", - minute: "2-digit", + hour: "numeric", + minute: "numeric", timeZoneName: "short", + weekday: "long", })} hoist placement="bottom" diff --git a/frontend/src/features/archived-items/crawl-logs.ts b/frontend/src/features/archived-items/crawl-logs.ts index 4fa366ad58..f9d13479e6 100644 --- a/frontend/src/features/archived-items/crawl-logs.ts +++ b/frontend/src/features/archived-items/crawl-logs.ts @@ -222,7 +222,10 @@ export class CrawlLogs extends BtrixElement { `, () => this.filter - ? emptyMessage({ message: emptyMessageFor[this.filter] }) + ? emptyMessage({ + classNames: tw`border-y`, + message: emptyMessageFor[this.filter], + }) : nothing, )} `; diff --git a/frontend/src/layouts/emptyMessage.ts b/frontend/src/layouts/emptyMessage.ts index ddaad6673f..06f33a6756 100644 --- a/frontend/src/layouts/emptyMessage.ts +++ b/frontend/src/layouts/emptyMessage.ts @@ -7,16 +7,18 @@ export function emptyMessage({ message, detail, actions, + classNames, }: { message: TemplateResult | string; detail?: TemplateResult | string; actions?: TemplateResult; + classNames?: typeof tw | string; }) { return html` -
+

@@ -42,7 +42,9 @@ export function page( class="mx-auto box-border flex min-h-full w-full max-w-screen-desktop flex-1 flex-col gap-3 p-3 lg:px-10 lg:pb-10" > ${header.breadcrumbs ? html` ${pageNav(header.breadcrumbs)} ` : nothing} - ${pageHeader(header)} -

${render()}
+ ${header.title ? pageHeader(header) : nothing} + ${header.title + ? html`
${render()}
` + : render()}
`; } diff --git a/frontend/src/layouts/panel.ts b/frontend/src/layouts/panel.ts index 6bfe56fb2a..f89c2022a0 100644 --- a/frontend/src/layouts/panel.ts +++ b/frontend/src/layouts/panel.ts @@ -1,8 +1,11 @@ +import clsx from "clsx"; import { html, type TemplateResult } from "lit"; import { ifDefined } from "lit/directives/if-defined.js"; import { pageHeading } from "./page"; +import { tw } from "@/utils/tailwind"; + export function panelHeader({ heading, actions, @@ -20,8 +23,18 @@ export function panelHeader({ `; } -export function panelBody({ content }: { content: TemplateResult }) { - return html`
${content}
`; +export function panelBody({ + content, + classNames, +}: { + content: TemplateResult; + classNames?: typeof tw | string; +}) { + return html`
+ ${content} +
`; } /** @@ -39,6 +52,10 @@ export function panel({ className?: string; } & Parameters[0]) { return html`
- ${panelHeader({ heading, actions })} ${body} + ${panelHeader({ heading, actions })} + + ${body}
`; } diff --git a/frontend/src/layouts/secondaryPanel.ts b/frontend/src/layouts/secondaryPanel.ts new file mode 100644 index 0000000000..bc3663950b --- /dev/null +++ b/frontend/src/layouts/secondaryPanel.ts @@ -0,0 +1,45 @@ +import clsx from "clsx"; +import { html, nothing, type TemplateResult } from "lit"; +import { ifDefined } from "lit/directives/if-defined.js"; + +import { panelBody } from "./panel"; + +import { tw } from "@/utils/tailwind"; + +export function secondaryHeading({ + heading, + id, + srOnly, +}: { + heading: string | TemplateResult; + id?: string; + srOnly?: boolean; +}) { + return html`
+ ${heading} +
`; +} + +export function secondaryPanel({ + heading, + body, + srOnly, +}: { + heading: string | TemplateResult; + body?: string | TemplateResult; + srOnly?: boolean; +}) { + return panelBody({ + content: html`
+ ${secondaryHeading({ id: "cardHeading", heading, srOnly })} + ${srOnly ? nothing : html``} +
${body}
+
`, + }); +} diff --git a/frontend/src/pages/org/browser-profiles/index.ts b/frontend/src/pages/org/browser-profiles/index.ts new file mode 100644 index 0000000000..366658f188 --- /dev/null +++ b/frontend/src/pages/org/browser-profiles/index.ts @@ -0,0 +1 @@ +import "./profile"; diff --git a/frontend/src/pages/org/browser-profiles/profile.ts b/frontend/src/pages/org/browser-profiles/profile.ts new file mode 100644 index 0000000000..7ee6a50d48 --- /dev/null +++ b/frontend/src/pages/org/browser-profiles/profile.ts @@ -0,0 +1,479 @@ +import { localized, msg, str } from "@lit/localize"; +import { Task } from "@lit/task"; +import { html, nothing, type TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import { when } from "lit/directives/when.js"; +import capitalize from "lodash/fp/capitalize"; +import queryString from "query-string"; + +import { BtrixElement } from "@/classes/BtrixElement"; +import { ClipboardController } from "@/controllers/clipboard"; +import { none } from "@/layouts/empty"; +import { emptyMessage } from "@/layouts/emptyMessage"; +import { page } from "@/layouts/page"; +import { panel, panelBody } from "@/layouts/panel"; +import { secondaryPanel } from "@/layouts/secondaryPanel"; +import { OrgTab } from "@/routes"; +import type { APIPaginatedList, APIPaginationQuery } from "@/types/api"; +import type { Profile, Workflow } from "@/types/crawler"; +import { SortDirection } from "@/types/utils"; +import { renderName } from "@/utils/crawler"; +import { isArchivingDisabled } from "@/utils/orgs"; +import { pluralOf } from "@/utils/pluralize"; + +@customElement("btrix-browser-profiles-profile-page") +@localized() +export class BrowserProfilesProfilePage extends BtrixElement { + @property({ type: String }) + profileId = ""; + + private get profile() { + return this.profileTask.value; + } + + private readonly profileTask = new Task(this, { + task: async ([profileId], { signal }) => { + return this.getProfile(profileId, signal); + }, + args: () => [this.profileId] as const, + }); + + private readonly workflowsTask = new Task(this, { + task: async ([profileId], { signal }) => { + return this.getWorkflows({ profileId, page: 1, pageSize: 10 }, signal); + }, + args: () => [this.profileId] as const, + }); + + render() { + const header = { + breadcrumbs: [ + { + href: `${this.navigate.orgBasePath}/${OrgTab.BrowserProfiles}`, + content: msg("Browser Profiles"), + }, + { + content: this.profile?.name, + }, + ], + title: html`${this.profile?.name ?? + html``} + ${when( + this.appState.isCrawler, + () => + html` + + `, + )} `, + secondary: this.profileTask.render({ + complete: (profile) => { + const isBackedUp = + profile.resource?.replicas && profile.resource.replicas.length > 0; + + return html`
+ + + ${profile.inUse ? msg("In Use") : msg("Not In Use")} + + + + ${isBackedUp ? msg("Backed Up") : msg("Not Backed Up")} + +
`; + }, + }), + actions: this.renderActions(), + } satisfies Parameters[0]; + + return html`${page(header, this.renderPage)}`; + } + + private renderActions() { + const archivingDisabled = isArchivingDisabled(this.org); + + return html` + + + ${msg("Open Profile")} + + + + ${msg("Actions")} + + + {}}> + + ${msg("Open Profile")} + + + ${when( + this.appState.isCrawler, + () => html` + {}}> + + ${msg("Configure Profile")} + + {}}> + + ${msg("Edit Metadata")} + + {}}> + + ${msg("Duplicate Profile")} + + + `, + )} + ClipboardController.copyToClipboard(this.profileId)} + > + + ${msg("Copy Profile ID")} + + ${when( + this.appState.isCrawler, + () => html` + + {}}> + + ${msg("Delete Profile")} + + `, + )} + + + `; + } + + private readonly renderPage = () => { + return html` +
+
+ ${this.renderConfig()} ${this.renderDescription()} +
+ +
+ ${this.renderInfo()} ${this.renderUsage()} +
+
+ `; + }; + + private renderConfig() { + const siteListSkeleton = () => + html``; + const content = html`
+ + + ${this.renderDetail((profile) => + profile.crawlerChannel + ? capitalize(profile.crawlerChannel) + : none, + )} + + + ${this.renderDetail((profile) => + profile.proxyId ? profile.proxyId : none, + )} + + +
+ +
+

${msg("Visited Sites")}

+
    + ${this.profileTask.render({ + initial: siteListSkeleton, + pending: siteListSkeleton, + complete: (profile) => + profile.origins.map( + (origin) => html` +
  • + + + +
    + + + + + +
    +
  • + `, + ), + })} +
+
`; + + return panel({ + heading: msg("Profile Configuration"), + actions: this.appState.isCrawler + ? html` + + ` + : undefined, + body: panelBody({ content }), + }); + } + + private renderDescription() { + const skeleton = () => + html` + + `; + + const content = html`
+ ${this.profileTask.render({ + initial: skeleton, + pending: skeleton, + complete: (profile) => + profile.description + ? html` +
+ ${profile.description} +
+ ` + : emptyMessage({ + message: msg("No description added."), + actions: this.appState.isCrawler + ? html` + + ${msg("Add Description")}` + : undefined, + }), + })} +
`; + + return panel({ + heading: msg("Description"), + actions: this.appState.isCrawler + ? html` + + ` + : undefined, + body: panelBody({ content }), + }); + } + + private renderInfo() { + return secondaryPanel({ + heading: msg("General Information"), + body: html` + + + + ${msg("Size")} + + + ${this.renderDetail((profile) => + this.localize.bytes(profile.resource?.size || 0), + )} + + + + + ${msg("Last Modified")} + + + ${this.renderDetail((profile) => + this.localize.relativeDate( + // NOTE older profiles may not have "modified" data + profile.modified || profile.created, + ), + )} + + + + + ${msg("Modified By")} + + + ${this.renderDetail( + (profile) => + profile.modifiedByName || profile.createdByName || none, + )} + + + ${when(this.profile, (profile) => + profile.created && profile.created !== profile.modified + ? html` + + + ${msg("Date Created")} + + + ${this.localize.date(profile.created, { + dateStyle: "medium", + })} + + + ` + : nothing, + )} + + `, + }); + } + + private renderUsage() { + const workflowListSkeleton = () => + html``; + + return secondaryPanel({ + heading: msg("Usage in Crawl Workflows"), + body: this.profileTask.render({ + initial: workflowListSkeleton, + pending: workflowListSkeleton, + complete: (profile) => + profile.inUse + ? this.workflowsTask.render({ + initial: workflowListSkeleton, + pending: workflowListSkeleton, + complete: this.renderWorkflows, + }) + : html`${emptyMessage({ + message: msg("Not used by any crawl workflows."), + actions: html` + + ${msg("Create Workflow Using Profile")}`, + })}`, + }), + }); + } + + private readonly renderWorkflows = ( + workflows: APIPaginatedList, + ) => { + const number_of_workflows = this.localize.number(workflows.total); + const plural_of_workflows = pluralOf("workflows", workflows.total); + + return html` +
+
+ ${msg("Most Recently Crawled")} +
+ + ${workflows.total > 1 + ? msg(str`View ${number_of_workflows} ${plural_of_workflows}`) + : msg("View")} + +
+
    + ${workflows.items.map( + (workflow) => + html`
  • +
    ${renderName(workflow)}
    +
    + + + +
    +
  • `, + )} +
+ `; + }; + + private readonly renderDetail = ( + render: (profile: Profile) => string | TemplateResult, + ) => + when( + this.profile, + render, + () => html``, + ); + + private async getProfile(profileId: string, signal: AbortSignal) { + const data = await this.api.fetch( + `/orgs/${this.orgId}/profiles/${profileId}`, + { signal }, + ); + + return data; + } + + private async getWorkflows( + params: { profileId: string } & APIPaginationQuery, + signal: AbortSignal, + ) { + const query = queryString.stringify({ + ...params, + profileIds: [params.profileId], + sortBy: "lastRun", + sortDirection: SortDirection.Descending, + }); + + const data = await this.api.fetch>( + `/orgs/${this.orgId}/crawlconfigs?${query}`, + { signal }, + ); + + return data; + } +} diff --git a/frontend/src/pages/org/collections-list.ts b/frontend/src/pages/org/collections-list.ts index ea7e5a69a0..336eeeb88f 100644 --- a/frontend/src/pages/org/collections-list.ts +++ b/frontend/src/pages/org/collections-list.ts @@ -521,6 +521,7 @@ export class CollectionsList extends WithSearchOrgContext(BtrixElement) { this.isCrawler, () => emptyMessage({ + classNames: tw`border-y`, message, detail: msg( "Collections let you easily organize, replay, and share multiple crawls.", @@ -542,6 +543,7 @@ export class CollectionsList extends WithSearchOrgContext(BtrixElement) { }), () => emptyMessage({ + classNames: tw`border-y`, message, }), )} diff --git a/frontend/src/pages/org/dashboard.ts b/frontend/src/pages/org/dashboard.ts index 33f10c6899..170e214e9e 100644 --- a/frontend/src/pages/org/dashboard.ts +++ b/frontend/src/pages/org/dashboard.ts @@ -300,9 +300,7 @@ export class Dashboard extends BtrixElement { ? this.renderMiscStorage(metrics) : nothing} - + ${this.renderStat({ value: metrics.archivedItemCount, singleLabel: msg("Archived Item"), @@ -364,9 +362,7 @@ export class Dashboard extends BtrixElement { class: tw`text-violet-600`, }, })} - + ${this.renderStat({ value: metrics.crawlPageCount, singleLabel: msg("Page Crawled"), diff --git a/frontend/src/pages/org/index.ts b/frontend/src/pages/org/index.ts index a02da08ed8..3f360fee93 100644 --- a/frontend/src/pages/org/index.ts +++ b/frontend/src/pages/org/index.ts @@ -54,7 +54,6 @@ import "./archived-item-detail"; import "./archived-items"; import "./collections-list"; import "./collection-detail"; -import "./browser-profiles-detail"; import "./browser-profiles-list"; import "./settings/settings"; import "./dashboard"; @@ -62,6 +61,7 @@ import "./dashboard"; import(/* webpackChunkName: "org" */ "./archived-item-qa/archived-item-qa"); import(/* webpackChunkName: "org" */ "./workflows-new"); import(/* webpackChunkName: "org" */ "./browser-profiles-new"); +import(/* webpackChunkName: "org" */ "./browser-profiles/profile"); const RESOURCE_NAMES = ["workflow", "collection", "browser-profile", "upload"]; type ResourceName = (typeof RESOURCE_NAMES)[number]; @@ -647,10 +647,9 @@ export class Org extends BtrixElement { const params = this.params as OrgParams["browser-profiles"]; if (params.browserProfileId) { - return html``; + >`; } if (params.browserId) { diff --git a/frontend/src/theme.stylesheet.css b/frontend/src/theme.stylesheet.css index 2ad070739a..d04f04cfc8 100644 --- a/frontend/src/theme.stylesheet.css +++ b/frontend/src/theme.stylesheet.css @@ -122,6 +122,12 @@ } @layer components { + sl-skeleton { + --border-radius: var(--sl-border-radius-small); + --color: var(--sl-color-neutral-50); + --sheen-color: var(--sl-color-neutral-100); + } + sl-avatar::part(base) { transition: var(--sl-transition-x-fast) background-color; } diff --git a/frontend/src/utils/pluralize.ts b/frontend/src/utils/pluralize.ts index 08862130c8..943ebb7df4 100644 --- a/frontend/src/utils/pluralize.ts +++ b/frontend/src/utils/pluralize.ts @@ -299,6 +299,32 @@ const plurals = { id: "changes.plural.other", }), }, + workflows: { + zero: msg("workflows", { + desc: 'plural form of "workflow" for zero workflows', + id: "workflows.plural.zero", + }), + one: msg("workflow", { + desc: 'singular form for "workflow"', + id: "workflows.plural.one", + }), + two: msg("workflows", { + desc: 'plural form of "workflow" for two workflows', + id: "workflows.plural.two", + }), + few: msg("workflows", { + desc: 'plural form of "workflow" for few workflows', + id: "workflows.plural.few", + }), + many: msg("workflows", { + desc: 'plural form of "workflow" for many workflows', + id: "workflows.plural.many", + }), + other: msg("workflows", { + desc: 'plural form of "workflow" for multiple/other workflows', + id: "workflows.plural.other", + }), + }, }; export const pluralOf = (word: keyof typeof plurals, count: number) => { From f6130eaa2df79f54d0c78a9fb412e973fc8b8c75 Mon Sep 17 00:00:00 2001 From: sua yoo Date: Wed, 5 Nov 2025 15:38:33 -0800 Subject: [PATCH 02/27] show workflows --- frontend/src/components/ui/code/index.ts | 4 +- frontend/src/components/ui/desc-list.ts | 1 + .../crawl-workflows/workflow-editor.ts | 14 +- .../features/crawl-workflows/workflow-list.ts | 131 +++-- .../src/pages/org/browser-profiles/profile.ts | 457 ++++++++++-------- frontend/src/stories/components/Code.ts | 8 +- 6 files changed, 373 insertions(+), 242 deletions(-) diff --git a/frontend/src/components/ui/code/index.ts b/frontend/src/components/ui/code/index.ts index 081e07b665..9af0351ac5 100644 --- a/frontend/src/components/ui/code/index.ts +++ b/frontend/src/components/ui/code/index.ts @@ -71,7 +71,7 @@ export class Code extends TailwindElement { language: Language = Language.XML; @property({ type: Boolean }) - wrap = true; + noWrap = false; async connectedCallback() { const languageFn = (await langaugeFiles[this.language]).default; @@ -94,7 +94,7 @@ export class Code extends TailwindElement { part="base" class=${clsx( tw`font-monospace m-0 text-neutral-600`, - this.wrap ? tw`whitespace-pre-wrap` : tw`whitespace-nowrap`, + this.noWrap ? tw`whitespace-nowrap` : tw`whitespace-pre-wrap`, )} >${staticHtml`${unsafeStatic(htmlStr)}`}`; } diff --git a/frontend/src/components/ui/desc-list.ts b/frontend/src/components/ui/desc-list.ts index a21ab6d8be..03d0ac58c0 100644 --- a/frontend/src/components/ui/desc-list.ts +++ b/frontend/src/components/ui/desc-list.ts @@ -127,6 +127,7 @@ export class DescList extends LitElement { vertical: !this.horizontal, horizontal: this.horizontal, })} + part="base" > `; diff --git a/frontend/src/features/crawl-workflows/workflow-editor.ts b/frontend/src/features/crawl-workflows/workflow-editor.ts index 88da94c53d..462a3e65f9 100644 --- a/frontend/src/features/crawl-workflows/workflow-editor.ts +++ b/frontend/src/features/crawl-workflows/workflow-editor.ts @@ -486,7 +486,19 @@ export class WorkflowEditor extends BtrixElement { (changedProperties.get("progressState") as ProgressState | undefined) ?.activeTab ) { - window.location.hash = this.progressState.activeTab; + const prevActiveTab = ( + changedProperties.get("progressState") as ProgressState | undefined + )?.activeTab; + + if (this.progressState.activeTab !== prevActiveTab) { + if (prevActiveTab) { + window.location.hash = this.progressState.activeTab; + } else { + const url = new URL(window.location.href); + url.hash = this.progressState.activeTab; + window.location.replace(url); + } + } } const prevFormState = changedProperties.get("formState") as | FormState diff --git a/frontend/src/features/crawl-workflows/workflow-list.ts b/frontend/src/features/crawl-workflows/workflow-list.ts index 00a9327e81..86d07113b5 100644 --- a/frontend/src/features/crawl-workflows/workflow-list.ts +++ b/frontend/src/features/crawl-workflows/workflow-list.ts @@ -32,6 +32,21 @@ import { humanizeSchedule } from "@/utils/cron"; import { srOnly, truncate } from "@/utils/css"; import { pluralOf } from "@/utils/pluralize"; +export type WorkflowColumnName = + | "name" + | "latest-crawl" + | "total-crawls" + | "modified" + | "actions"; + +const columnWidths = { + name: "minmax(18rem, 1fr)", + "latest-crawl": "minmax(15rem, 18rem)", + "total-crawls": "minmax(6rem, 9rem)", + modified: "minmax(12rem, 15rem)", + actions: "3rem", +} as const satisfies Record; + // postcss-lit-disable-next-line const mediumBreakpointCss = css`30rem`; // postcss-lit-disable-next-line @@ -50,7 +65,8 @@ const rowCss = css` } @media only screen and (min-width: ${largeBreakpointCss}) { .row { - grid-template-columns: 1fr 17rem 10rem 11rem 3rem; + grid-template-columns: var(--btrix-workflow-list-columns); + white-space: nowrap; } } @@ -231,33 +247,27 @@ export class WorkflowListItem extends BtrixElement { @property({ type: Object }) workflow?: ListWorkflow; + /** + * Limit columns displayed + * @TODO Convert to btrix-data-grid to make columns configurable + */ + @property({ type: Array }) + columns?: WorkflowColumnName[]; + @query(".row") row!: HTMLElement; @query("btrix-overflow-dropdown") dropdownMenu!: OverflowDropdown; - render() { - return html`
{ - if (e.target === this.dropdownMenu) { - return; - } - e.preventDefault(); - await this.updateComplete; - const failedStates = ["failed", "failed_not_logged_in"]; - const href = `/orgs/${this.orgSlugState}/workflows/${this.workflow?.id}/${failedStates.includes(this.workflow?.lastCrawlState || "") ? WorkflowTab.Logs : WorkflowTab.LatestCrawl}`; - this.navigate.to(href); - }} - > -
+ private readonly columnTemplate = { + name: () => + html`
${when(this.workflow?.shareable, ShareableNotice)} ${this.safeRender(this.renderName)}
-
+
${this.safeRender((workflow) => { if (workflow.schedule) { return humanizeSchedule(workflow.schedule, { @@ -270,9 +280,11 @@ export class WorkflowListItem extends BtrixElement { return msg("---"); })}
-
-
${this.safeRender(this.renderLatestCrawl)}
-
+
`, + "latest-crawl": () => + html`
${this.safeRender(this.renderLatestCrawl)}
`, + "total-crawls": () => + html`
${this.safeRender((workflow) => { if ( @@ -316,9 +328,11 @@ export class WorkflowListItem extends BtrixElement { `${this.localize.number(workflow.crawlCount, { notation: "compact" })} ${pluralOf("crawls", workflow.crawlCount)}`, )}
-
-
${this.safeRender(this.renderModifiedBy)}
-
+
`, + modified: () => + html`
${this.safeRender(this.renderModifiedBy)}
`, + actions: () => + html`
-
+
`, + } satisfies Record TemplateResult>; + + render() { + return html`
{ + if (e.target === this.dropdownMenu) { + return; + } + e.preventDefault(); + await this.updateComplete; + const failedStates = ["failed", "failed_not_logged_in"]; + const href = `/orgs/${this.orgSlugState}/workflows/${this.workflow?.id}/${failedStates.includes(this.workflow?.lastCrawlState || "") ? WorkflowTab.Logs : WorkflowTab.LatestCrawl}`; + this.navigate.to(href); + }} + > + ${this.columns + ? this.columns.map((col) => this.columnTemplate[col]()) + : Object.values(this.columnTemplate).map((render) => render())}
`; } @@ -551,18 +585,41 @@ export class WorkflowList extends LitElement { `, ]; + /** + * Limit columns displayed + * * @TODO Convert to btrix-data-grid to make columns configurable + */ + @property({ type: Array, noAccessor: true }) + columns?: WorkflowColumnName[]; + @queryAssignedElements({ selector: "btrix-workflow-list-item" }) - listItems!: HTMLElement[]; + listItems!: WorkflowListItem[]; + + static ColumnTemplate = { + name: html`
${msg(html`Name & Schedule`)}
`, + "latest-crawl": html`
${msg("Latest Crawl")}
`, + "total-crawls": html`
${msg("Total Size")}
`, + modified: html`
${msg("Last Modified")}
`, + actions: html`
+ ${msg("Actions")} +
`, + } satisfies Record; + + connectedCallback(): void { + this.style.setProperty( + "--btrix-workflow-list-columns", + this.columns?.map((col) => columnWidths[col]).join(" ") || + Object.values(columnWidths).join(" "), + ); + + super.connectedCallback(); + } render() { - return html`
-
${msg(html`Name & Schedule`)}
-
${msg("Latest Crawl")}
-
${msg("Total Size")}
-
${msg("Last Modified")}
-
- ${msg("Actions")} -
+ return html`
+ ${this.columns + ? this.columns.map((col) => WorkflowList.ColumnTemplate[col]) + : Object.values(WorkflowList.ColumnTemplate)}
@@ -574,6 +631,12 @@ export class WorkflowList extends LitElement { if (!el.attributes.getNamedItem("role")) { el.setAttribute("role", "listitem"); } + + if (this.columns) { + if (!el["columns"]) { + el["columns"] = this.columns; + } + } }); } } diff --git a/frontend/src/pages/org/browser-profiles/profile.ts b/frontend/src/pages/org/browser-profiles/profile.ts index 7ee6a50d48..c7a6bcefec 100644 --- a/frontend/src/pages/org/browser-profiles/profile.ts +++ b/frontend/src/pages/org/browser-profiles/profile.ts @@ -1,25 +1,43 @@ -import { localized, msg, str } from "@lit/localize"; +import { localized, msg } from "@lit/localize"; import { Task } from "@lit/task"; -import { html, nothing, type TemplateResult } from "lit"; -import { customElement, property } from "lit/decorators.js"; +import { html, type TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; import { when } from "lit/directives/when.js"; import capitalize from "lodash/fp/capitalize"; import queryString from "query-string"; import { BtrixElement } from "@/classes/BtrixElement"; +import type { + BtrixFilterChipChangeEvent, + FilterChip, +} from "@/components/ui/filter-chip"; +import { parsePage, type PageChangeEvent } from "@/components/ui/pagination"; import { ClipboardController } from "@/controllers/clipboard"; +import { CrawlStatus } from "@/features/archived-items/crawl-status"; +import type { WorkflowColumnName } from "@/features/crawl-workflows/workflow-list"; import { none } from "@/layouts/empty"; import { emptyMessage } from "@/layouts/emptyMessage"; import { page } from "@/layouts/page"; import { panel, panelBody } from "@/layouts/panel"; -import { secondaryPanel } from "@/layouts/secondaryPanel"; import { OrgTab } from "@/routes"; +import { stringFor } from "@/strings/ui"; import type { APIPaginatedList, APIPaginationQuery } from "@/types/api"; import type { Profile, Workflow } from "@/types/crawler"; +import type { CrawlState } from "@/types/crawlState"; import { SortDirection } from "@/types/utils"; -import { renderName } from "@/utils/crawler"; +import { isNotEqual } from "@/utils/is-not-equal"; import { isArchivingDisabled } from "@/utils/orgs"; import { pluralOf } from "@/utils/pluralize"; +import { tw } from "@/utils/tailwind"; + +const INITIAL_PAGE_SIZE = 5; +const WORKFLOW_PAGE_QUERY = "workflowsPage"; + +const workflowColumns = [ + "name", + "latest-crawl", + "actions", +] as const satisfies WorkflowColumnName[]; @customElement("btrix-browser-profiles-profile-page") @localized() @@ -27,6 +45,16 @@ export class BrowserProfilesProfilePage extends BtrixElement { @property({ type: String }) profileId = ""; + @state({ hasChanged: isNotEqual }) + private workflowParams: Required & { + lastCrawlState?: CrawlState[]; + } = { + page: parsePage( + new URLSearchParams(location.search).get(WORKFLOW_PAGE_QUERY), + ), + pageSize: INITIAL_PAGE_SIZE, + }; + private get profile() { return this.profileTask.value; } @@ -39,10 +67,10 @@ export class BrowserProfilesProfilePage extends BtrixElement { }); private readonly workflowsTask = new Task(this, { - task: async ([profileId], { signal }) => { - return this.getWorkflows({ profileId, page: 1, pageSize: 10 }, signal); + task: async ([profileId, workflowParams], { signal }) => { + return this.getWorkflows({ ...workflowParams, profileId }, signal); }, - args: () => [this.profileId] as const, + args: () => [this.profileId, this.workflowParams] as const, }); render() { @@ -101,18 +129,14 @@ export class BrowserProfilesProfilePage extends BtrixElement { const archivingDisabled = isArchivingDisabled(this.org); return html` - - - ${msg("Open Profile")} - ${msg("Actions")} - {}}> - - ${msg("Open Profile")} + {}}> + + ${msg("Inspect Profile")} ${when( @@ -156,18 +180,12 @@ export class BrowserProfilesProfilePage extends BtrixElement { private readonly renderPage = () => { return html` -
-
- ${this.renderConfig()} ${this.renderDescription()} +
+
+ ${this.renderConfig()} ${this.renderUsage()}
-
- ${this.renderInfo()} ${this.renderUsage()} -
+
${this.renderOverview()}
`; }; @@ -175,24 +193,24 @@ export class BrowserProfilesProfilePage extends BtrixElement { private renderConfig() { const siteListSkeleton = () => html``; - const content = html`
- - - ${this.renderDetail((profile) => - profile.crawlerChannel - ? capitalize(profile.crawlerChannel) - : none, - )} - - - ${this.renderDetail((profile) => - profile.proxyId ? profile.proxyId : none, - )} - - -
-
+ const settings = html`
+ + + ${this.renderDetail((profile) => + profile.crawlerChannel ? capitalize(profile.crawlerChannel) : none, + )} + + + ${this.renderDetail((profile) => + profile.proxyId ? profile.proxyId : none, + )} + + +
`; + + const origins = html` +

${msg("Visited Sites")}

    ${this.profileTask.render({ @@ -206,20 +224,22 @@ export class BrowserProfilesProfilePage extends BtrixElement { >
    @@ -242,128 +262,83 @@ export class BrowserProfilesProfilePage extends BtrixElement { ), })}
-
`; +
+ `; return panel({ - heading: msg("Profile Configuration"), - actions: this.appState.isCrawler - ? html` - - ` - : undefined, - body: panelBody({ content }), + heading: msg("Configuration"), + actions: html` +
+ ${this.appState.isCrawler + ? html` + + ` + : undefined} + + + + ${msg("Inspect")} + +
+ `, + body: panelBody({ content: html` ${origins} ${settings} ` }), }); } - private renderDescription() { - const skeleton = () => - html` - - `; - - const content = html`
- ${this.profileTask.render({ - initial: skeleton, - pending: skeleton, - complete: (profile) => - profile.description - ? html` -
- ${profile.description} -
- ` - : emptyMessage({ - message: msg("No description added."), - actions: this.appState.isCrawler - ? html` - - ${msg("Add Description")}` - : undefined, - }), - })} -
`; - + private renderOverview() { return panel({ - heading: msg("Description"), + heading: msg("Overview"), actions: this.appState.isCrawler - ? html` + ? html` ` : undefined, - body: panelBody({ content }), - }); - } - - private renderInfo() { - return secondaryPanel({ - heading: msg("General Information"), - body: html` - - - - ${msg("Size")} - - - ${this.renderDetail((profile) => - this.localize.bytes(profile.resource?.size || 0), - )} - - - - - ${msg("Last Modified")} - - - ${this.renderDetail((profile) => - this.localize.relativeDate( - // NOTE older profiles may not have "modified" data - profile.modified || profile.created, - ), - )} - - - - - ${msg("Modified By")} - - - ${this.renderDetail( - (profile) => - profile.modifiedByName || profile.createdByName || none, - )} - - - ${when(this.profile, (profile) => - profile.created && profile.created !== profile.modified - ? html` - - + + ${this.renderDetail((profile) => + profile.description + ? html` +
- ${msg("Date Created")} - - - ${this.localize.date(profile.created, { - dateStyle: "medium", - })} - - - ` - : nothing, - )} - - `, + ${profile.description} +
+ ` + : stringFor.none, + )} +
+ + ${this.renderDetail(() => html`${stringFor.none}`)} + + + + + + ${this.renderDetail((profile) => + this.localize.bytes(profile.resource?.size || 0), + )} + + + ${this.renderDetail((profile) => + this.localize.relativeDate( + // NOTE older profiles may not have "modified" data + profile.modified || profile.created, + ), + )} + + + ${this.renderDetail((profile) => { + const userName = profile.modifiedByName || profile.createdByName; + if (userName) { + return `${msg("Updated by")} ${userName}`; + } + + return stringFor.notApplicable; + })} + + + `, }); } @@ -371,18 +346,36 @@ export class BrowserProfilesProfilePage extends BtrixElement { const workflowListSkeleton = () => html``; - return secondaryPanel({ - heading: msg("Usage in Crawl Workflows"), - body: this.profileTask.render({ + return panel({ + heading: msg("Usage"), + body: html`${this.profileTask.render({ initial: workflowListSkeleton, pending: workflowListSkeleton, complete: (profile) => profile.inUse - ? this.workflowsTask.render({ - initial: workflowListSkeleton, - pending: workflowListSkeleton, - complete: this.renderWorkflows, - }) + ? html` +
+ + + ${this.workflowsTask.value + ? `${this.localize.number(this.workflowsTask.value.total)} ${pluralOf("workflows", this.workflowsTask.value.total)}` + : html``} + + + ${msg("No")} + + +
+ + ${this.workflowsTask.render({ + initial: workflowListSkeleton, + pending: () => + this.workflowsTask.value + ? this.renderWorkflows(this.workflowsTask.value) + : workflowListSkeleton(), + complete: this.renderWorkflows, + })} + ` : html`${emptyMessage({ message: msg("Not used by any crawl workflows."), actions: html` @@ -390,53 +383,111 @@ export class BrowserProfilesProfilePage extends BtrixElement { ${msg("Create Workflow Using Profile")}`, })}`, - }), + })}`, }); } private readonly renderWorkflows = ( workflows: APIPaginatedList, ) => { - const number_of_workflows = this.localize.number(workflows.total); - const plural_of_workflows = pluralOf("workflows", workflows.total); + const failedNotLoggedInState = "failed_not_logged_in" satisfies CrawlState; return html` -
-
- ${msg("Most Recently Crawled")} +
+
+ + ${msg("Filter by:")} + + + { + const { checked } = e.target as FilterChip; + + this.workflowParams = { + ...this.workflowParams, + lastCrawlState: checked ? [failedNotLoggedInState] : undefined, + }; + }} + > + ${CrawlStatus.getContent({ state: failedNotLoggedInState }).label} +
- - ${workflows.total > 1 - ? msg(str`View ${number_of_workflows} ${plural_of_workflows}`) - : msg("View")} -
-
    - ${workflows.items.map( - (workflow) => - html`
  • -
    ${renderName(workflow)}
    -
    - - - -
    -
  • `, - )} -
+ + ${workflows.total + ? html` + + ${workflows.items.map( + (workflow) => + html` + + ${when( + this.appState.isCrawler, + () => html` + + + ${msg("Edit Workflow Settings")} + + + `, + )} + + + ${msg("Go to Workflow")} + + + `, + )} + + +
+ { + this.workflowParams = { + ...this.workflowParams, + page: e.detail.page, + }; + }} + > + +
+ ` + : emptyMessage({ + classNames: tw`border-y`, + message: msg("No matching workflows found."), + actions: html` + + (this.workflowParams = { + ...this.workflowParams, + lastCrawlState: undefined, + })} + > + + ${msg("Clear filters")} + `, + })} `; }; diff --git a/frontend/src/stories/components/Code.ts b/frontend/src/stories/components/Code.ts index d57d13d828..2a768c69fc 100644 --- a/frontend/src/stories/components/Code.ts +++ b/frontend/src/stories/components/Code.ts @@ -9,12 +9,16 @@ type Language = `${Code["language"]}`; export type RenderProps = Omit & { language: Language }; -export const renderCode = ({ language, value, wrap }: Partial) => { +export const renderCode = ({ + language, + value, + noWrap, +}: Partial) => { return html` `; From 4876f7a0bb82c58cd95fc660a4f831c579aca8db Mon Sep 17 00:00:00 2001 From: sua yoo Date: Wed, 5 Nov 2025 20:03:48 -0800 Subject: [PATCH 03/27] metadata dialog --- .../src/features/browser-profiles/index.ts | 1 + .../profile-metadata-dialog.ts | 199 +++++++++++++++++ .../src/pages/org/browser-profiles/profile.ts | 211 ++++++++++-------- 3 files changed, 319 insertions(+), 92 deletions(-) create mode 100644 frontend/src/features/browser-profiles/profile-metadata-dialog.ts diff --git a/frontend/src/features/browser-profiles/index.ts b/frontend/src/features/browser-profiles/index.ts index 7e843968d9..020a81f967 100644 --- a/frontend/src/features/browser-profiles/index.ts +++ b/frontend/src/features/browser-profiles/index.ts @@ -1,3 +1,4 @@ import("./new-browser-profile-dialog"); import("./profile-browser"); +import("./profile-metadata-dialog"); import("./select-browser-profile"); diff --git a/frontend/src/features/browser-profiles/profile-metadata-dialog.ts b/frontend/src/features/browser-profiles/profile-metadata-dialog.ts new file mode 100644 index 0000000000..7ba00a9817 --- /dev/null +++ b/frontend/src/features/browser-profiles/profile-metadata-dialog.ts @@ -0,0 +1,199 @@ +import { localized, msg } from "@lit/localize"; +import { Task, TaskStatus } from "@lit/task"; +import type { SlInput, SlTextarea } from "@shoelace-style/shoelace"; +import { serialize } from "@shoelace-style/shoelace/dist/utilities/form.js"; +import { html } from "lit"; +import { customElement, property, query, state } from "lit/decorators.js"; + +import { BtrixElement } from "@/classes/BtrixElement"; +import type { Dialog } from "@/components/ui/dialog"; +import type { Profile } from "@/types/crawler"; +import { isApiError } from "@/utils/api"; +import { maxLengthValidator } from "@/utils/form"; + +export type ProfileUpdatedEvent = CustomEvent<{ + name: Profile["name"]; + description: Profile["description"]; +}>; + +/** + * @fires btrix-updated + */ +@customElement("btrix-profile-metadata-dialog") +@localized() +export class ProfileMetadataDialog extends BtrixElement { + @property({ type: Object }) + profile?: Profile; + + @property({ type: Boolean }) + open = false; + + @property({ type: String }) + autofocusOn?: "name" | "description"; + + @state() + private isDialogVisible = false; + + @query("btrix-dialog") + private readonly dialog?: Dialog | null; + + @query("form") + private readonly form?: HTMLFormElement | null; + + @query(`sl-input[name="name"]`) + private readonly nameInput?: SlInput | null; + + @query(`sl-textarea[name="description"]`) + private readonly descriptionInput?: SlTextarea | null; + + private readonly validateNameMax = maxLengthValidator(50); + private readonly validateDescriptionMax = maxLengthValidator(500); + + private readonly submitTask = new Task(this, { + autoRun: false, + task: async ([profile], { signal }) => { + if (!this.form || !profile) { + console.debug("no form or profile", this.form, profile); + return; + } + + const params = serialize(this.form) as { + name: string; + description: string; + }; + + try { + await this.api.fetch<{ updated: boolean }>( + `/orgs/${this.orgId}/profiles/${profile.id}`, + { + method: "PATCH", + body: JSON.stringify(params), + signal, + }, + ); + + this.dispatchEvent( + new CustomEvent("btrix-updated", { + detail: params, + }), + ); + + this.notify.toast({ + message: msg("Browser profile metadata updated."), + variant: "success", + icon: "check2-circle", + id: "browser-profile-save-status", + }); + } 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.toast({ + message: message, + variant: "danger", + icon: "exclamation-octagon", + id: "browser-profile-save-status", + }); + } + }, + args: () => [this.profile] as const, + }); + + render() { + return html` (this.isDialogVisible = true)} + @sl-after-show=${() => { + switch (this.autofocusOn) { + case "name": + this.nameInput?.focus(); + break; + case "description": + this.descriptionInput?.focus(); + break; + default: + break; + } + }} + @sl-after-hide=${() => (this.isDialogVisible = false)} + >${this.isDialogVisible ? this.renderForm() : ""} + `; + } + + private renderForm() { + if (!this.profile) return; + + const submitting = this.submitTask.status === TaskStatus.PENDING; + + return html` +
{ + e.preventDefault(); + + if (this.form?.checkValidity()) { + void this.submitTask.run(); + } + }} + @reset=${() => void this.dialog?.hide()} + > + + + + + +
+
+ ${msg("Cancel")} + ${msg("Save")} +
+ `; + } +} diff --git a/frontend/src/pages/org/browser-profiles/profile.ts b/frontend/src/pages/org/browser-profiles/profile.ts index c7a6bcefec..fee030174f 100644 --- a/frontend/src/pages/org/browser-profiles/profile.ts +++ b/frontend/src/pages/org/browser-profiles/profile.ts @@ -2,6 +2,7 @@ import { localized, msg } from "@lit/localize"; import { Task } from "@lit/task"; import { html, type TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators.js"; +import { ifDefined } from "lit/directives/if-defined.js"; import { when } from "lit/directives/when.js"; import capitalize from "lodash/fp/capitalize"; import queryString from "query-string"; @@ -55,6 +56,9 @@ export class BrowserProfilesProfilePage extends BtrixElement { pageSize: INITIAL_PAGE_SIZE, }; + @state() + private openDialog?: "metadata" | "metadata-name" | "metadata-description"; + private get profile() { return this.profileTask.value; } @@ -74,6 +78,33 @@ export class BrowserProfilesProfilePage extends BtrixElement { }); render() { + const badges = (profile: Profile) => { + const isBackedUp = + profile.resource?.replicas && profile.resource.replicas.length > 0; + + return html`
+ + + ${profile.inUse ? msg("In Use") : msg("Not In Use")} + + + + ${isBackedUp ? msg("Backed Up") : msg("Not Backed Up")} + +
`; + }; + const badgesSkeleton = () => + html`
+ + +
`; + const header = { breadcrumbs: [ { @@ -93,36 +124,36 @@ export class BrowserProfilesProfilePage extends BtrixElement { (this.openDialog = "metadata-name")} > `, )} `, - secondary: this.profileTask.render({ - complete: (profile) => { - const isBackedUp = - profile.resource?.replicas && profile.resource.replicas.length > 0; - - return html`
- - - ${profile.inUse ? msg("In Use") : msg("Not In Use")} - - - - ${isBackedUp ? msg("Backed Up") : msg("Not Backed Up")} - -
`; - }, - }), + secondary: when(this.profile, badges, badgesSkeleton), actions: this.renderActions(), } satisfies Parameters[0]; - return html`${page(header, this.renderPage)}`; + return html`${page(header, this.renderPage)} + ${when( + this.profile, + (profile) => + html` (this.openDialog = undefined)} + @btrix-updated=${() => { + void this.profileTask.run(); + this.openDialog = undefined; + }} + > + `, + )} `; } private renderActions() { @@ -146,7 +177,7 @@ export class BrowserProfilesProfilePage extends BtrixElement { ${msg("Configure Profile")} - {}}> + (this.openDialog = "metadata")}> ${msg("Edit Metadata")} @@ -191,7 +222,7 @@ export class BrowserProfilesProfilePage extends BtrixElement { }; private renderConfig() { - const siteListSkeleton = () => + const originsSkeleton = () => html``; const settings = html`
@@ -209,58 +240,46 @@ export class BrowserProfilesProfilePage extends BtrixElement {
`; - const origins = html` + const origins = (profile: Profile) => + profile.origins.map( + (origin) => html` +
  • + + + +
    + + + + + +
    +
  • + `, + ); + + const siteList = html`

    ${msg("Visited Sites")}

      - ${this.profileTask.render({ - initial: siteListSkeleton, - pending: siteListSkeleton, - complete: (profile) => - profile.origins.map( - (origin) => html` -
    • - - - -
      - - - - - -
      -
    • - `, - ), - })} + ${when(this.profile, origins, originsSkeleton)}
    `; @@ -281,7 +300,7 @@ export class BrowserProfilesProfilePage extends BtrixElement {
    `, - body: panelBody({ content: html` ${origins} ${settings} ` }), + body: panelBody({ content: html` ${siteList} ${settings} ` }), }); } @@ -290,7 +309,11 @@ export class BrowserProfilesProfilePage extends BtrixElement { heading: msg("Overview"), actions: this.appState.isCrawler ? html` - + (this.openDialog = "metadata-description")} + > ` : undefined, body: html` @@ -299,11 +322,11 @@ export class BrowserProfilesProfilePage extends BtrixElement { ${this.renderDetail((profile) => profile.description ? html` +
    ${profile.description}
    - ${profile.description} -
    ` : stringFor.none, )} @@ -348,10 +371,9 @@ export class BrowserProfilesProfilePage extends BtrixElement { return panel({ heading: msg("Usage"), - body: html`${this.profileTask.render({ - initial: workflowListSkeleton, - pending: workflowListSkeleton, - complete: (profile) => + body: when( + this.profile, + (profile) => profile.inUse ? html`
    @@ -376,14 +398,19 @@ export class BrowserProfilesProfilePage extends BtrixElement { complete: this.renderWorkflows, })} ` - : html`${emptyMessage({ - message: msg("Not used by any crawl workflows."), - actions: html` - - ${msg("Create Workflow Using Profile")}`, - })}`, - })}`, + : panelBody({ + content: emptyMessage({ + message: msg( + "This profile is not in use by any crawl workflows.", + ), + actions: html` + + ${msg("Create Workflow Using Profile")}`, + }), + }), + workflowListSkeleton, + ), }); } From 1d4dcce5e43ead33d3aed5b46f67e048cb8e5566 Mon Sep 17 00:00:00 2001 From: sua yoo Date: Wed, 5 Nov 2025 21:48:32 -0800 Subject: [PATCH 04/27] render browser --- .../browser-profiles/profile-browser.ts | 84 +++++--- .../src/pages/org/browser-profiles/profile.ts | 188 +++++++++++++++--- 2 files changed, 214 insertions(+), 58 deletions(-) diff --git a/frontend/src/features/browser-profiles/profile-browser.ts b/frontend/src/features/browser-profiles/profile-browser.ts index 8248820a0e..af7b94d840 100644 --- a/frontend/src/features/browser-profiles/profile-browser.ts +++ b/frontend/src/features/browser-profiles/profile-browser.ts @@ -1,10 +1,12 @@ import { localized, msg, str } from "@lit/localize"; +import clsx from "clsx"; import { html, type PropertyValues } from "lit"; import { customElement, property, query, state } from "lit/decorators.js"; import { when } from "lit/directives/when.js"; import { BtrixElement } from "@/classes/BtrixElement"; import { isApiError, type APIError } from "@/utils/api"; +import { tw } from "@/utils/tailwind"; const POLL_INTERVAL_SECONDS = 2; const hiddenClassList = ["translate-x-2/3", "opacity-0", "pointer-events-none"]; @@ -50,6 +52,9 @@ export class ProfileBrowser extends BtrixElement { @property({ type: Array }) origins?: string[]; + @property({ type: Boolean }) + readOnly = false; + @state() private iframeSrc?: string; @@ -100,9 +105,11 @@ export class ProfileBrowser extends BtrixElement { super.disconnectedCallback(); } - private onBeforeUnload(e: BeforeUnloadEvent) { - e.preventDefault(); - } + private readonly onBeforeUnload = (e: BeforeUnloadEvent) => { + if (!this.readOnly) { + e.preventDefault(); + } + }; willUpdate(changedProperties: PropertyValues & Map) { if (changedProperties.has("browserId")) { @@ -160,7 +167,7 @@ export class ProfileBrowser extends BtrixElement { render() { return html` -
    +
    ${this.renderControlBar()}
    ${this.renderSidebarButton()} - + void document.exitFullscreen()} @@ -206,20 +213,24 @@ export class ProfileBrowser extends BtrixElement { return html`
    -
    +
    ${msg("Interactive Browser")} - - - + +
    ${this.renderSidebarButton()} - + void this.enterFullscreen()} @@ -233,7 +244,7 @@ export class ProfileBrowser extends BtrixElement { private renderBrowser() { if (this.browserNotAvailable) { return html` -
    +

    ${msg(`Interactive browser session timed out due to inactivity.`)} @@ -250,9 +261,12 @@ export class ProfileBrowser extends BtrixElement { } if (this.iframeSrc) { - return html`

    + return html`
    - ${when( - this.browserDisconnected, - () => html` -
    - -

    - ${msg( - "Connection to interactive browser lost. Waiting to reconnect...", - )} -

    -
    -
    - `, - )} -
    `; - } - - if (this.browserId && !this.isIframeLoaded) { - return html` -
    -

    - ${msg("Loading interactive browser...")} -

    - -
    - `; - } - - return ""; - } + private readonly renderBrowser = (browser: BrowserResponseData) => { + return html`
    + ${when( + browser.url, + (url) => html` + + `, + )} + ${this.originsTask.render({ + error: () => html` +
    + +

    + ${msg( + "Connection to interactive browser lost. Waiting to reconnect...", + )} +

    +
    +
    + `, + })} +
    `; + }; private renderSidebarButton() { return html` @@ -329,7 +435,7 @@ export class ProfileBrowser extends BtrixElement { `; } - private renderOrigins() { + private renderOrigins(origins: string[]) { return html`

      - ${this.origins?.map((url) => this.renderOriginItem(url))} + ${origins.map((url) => this.renderOriginItem(url))}
    `; } - private renderNewOrigins() { - if (!this.newOrigins?.length) return; + private renderNewOrigins(origins: string[]) { + if (!origins.length) return; return html`

    @@ -370,22 +476,25 @@ export class ProfileBrowser extends BtrixElement { >

      - ${this.newOrigins.map((url) => this.renderOriginItem(url))} + ${origins.map((url) => this.renderOriginItem(url))}
    `; } private renderOriginItem(url: string) { + const iframeSrc = + !isPolling(this.browserTask.value) && this.browserTask.value?.url; + return html`
  • (this.iframeSrc ? this.navigateBrowser({ url }) : {})} + @click=${() => (iframeSrc ? this.navigateBrowser({ url }) : {})} >
    ${url}
    - ${this.iframeSrc + ${iframeSrc ? html`` : ""}
  • `; @@ -395,78 +504,10 @@ export class ProfileBrowser extends BtrixElement { this.dispatchEvent(new CustomEvent("btrix-browser-reload")); } - /** - * Fetch browser profile and update internal state - */ - private async fetchBrowser(): Promise { - await this.updateComplete; - - this.iframeSrc = undefined; - this.isIframeLoaded = false; - - await this.checkBrowserStatus(); - } - - /** - * Check whether temporary browser is up - **/ - private async checkBrowserStatus() { - let result: BrowserResponseData; - try { - result = await this.getBrowser(); - this.browserNotAvailable = false; - } catch (e) { - this.browserNotAvailable = true; - return; - } - - if (result.detail === "waiting_for_browser") { - this.pollTimerId = window.setTimeout( - () => void this.checkBrowserStatus(), - POLL_INTERVAL_SECONDS * 1000, - ); - - return; - } else if (result.url) { - // check that the browser is actually available - // if not, continue waiting - // (will not work with local frontend due to CORS) - try { - const resp = await fetch(result.url, { method: "HEAD" }); - if (!resp.ok) { - this.pollTimerId = window.setTimeout( - () => void this.checkBrowserStatus(), - POLL_INTERVAL_SECONDS * 1000, - ); - return; - } - } catch (e) { - // ignore - } - - if (this.initialNavigateUrl) { - await this.navigateBrowser({ url: this.initialNavigateUrl }); - } - - this.iframeSrc = result.url; - - await this.updateComplete; - - this.dispatchEvent( - new CustomEvent("btrix-browser-load", { - detail: result.url, - }), - ); - - void this.pingBrowser(); - } else { - console.debug("Unknown checkBrowserStatus state"); - } - } - - private async getBrowser() { + private async getBrowser(browserId: string, signal?: AbortSignal) { const data = await this.api.fetch( - `/orgs/${this.orgId}/profiles/browser/${this.browserId}`, + `/orgs/${this.orgId}/profiles/browser/${browserId}`, + { signal }, ); return data; @@ -476,8 +517,6 @@ export class ProfileBrowser extends BtrixElement { * Navigate to URL in temporary browser **/ private async navigateBrowser({ url }: { url: string }) { - if (!this.iframeSrc) return; - const data = this.api.fetch( `/orgs/${this.orgId}/profiles/browser/${this.browserId}/navigate`, { @@ -492,42 +531,13 @@ export class ProfileBrowser extends BtrixElement { /** * Ping temporary browser to keep it alive **/ - private async pingBrowser() { - if (!this.iframeSrc) return; - - try { - const data = await this.api.fetch<{ origins?: string[] }>( - `/orgs/${this.orgId}/profiles/browser/${this.browserId}/ping`, - { - method: "POST", - }, - ); - - if (!this.origins) { - this.origins = data.origins; - } else { - const origins = this.origins; - - this.newOrigins = data.origins?.filter( - (url: string) => - !origins.includes(url) && !origins.includes(url.replace(/\/$/, "")), - ); - } - - this.browserDisconnected = false; - } catch (e) { - if (isApiError(e) && e.details === "no_such_browser") { - this.browserNotAvailable = true; - } else { - this.browserDisconnected = true; - } - - await this.updateComplete; - } - - this.pollTimerId = window.setTimeout( - () => void this.pingBrowser(), - POLL_INTERVAL_SECONDS * 1000, + private async pingBrowser(browserId: string, signal?: AbortSignal) { + return this.api.fetch<{ origins?: string[] }>( + `/orgs/${this.orgId}/profiles/browser/${browserId}/ping`, + { + method: "POST", + signal, + }, ); } @@ -545,16 +555,17 @@ export class ProfileBrowser extends BtrixElement { } } - private onIframeLoad() { - this.isIframeLoaded = true; + private async onIframeLoad(url: string) { try { this.iframe?.contentWindow?.localStorage.setItem("uiTheme", '"default"'); - } catch (e) { - /* empty */ + } catch (err) { + console.debug(err); } + + await this.updateComplete; this.dispatchEvent( new CustomEvent("btrix-browser-load", { - detail: this.iframeSrc, + detail: url, }), ); } diff --git a/frontend/src/pages/org/browser-profiles-detail.ts b/frontend/src/pages/org/browser-profiles-detail.ts index d57abb7a23..01fa76cc82 100644 --- a/frontend/src/pages/org/browser-profiles-detail.ts +++ b/frontend/src/pages/org/browser-profiles-detail.ts @@ -221,7 +221,6 @@ export class BrowserProfilesDetail extends BtrixElement { (this.isBrowserLoaded = true)} @btrix-browser-error=${this.onBrowserError} diff --git a/frontend/src/pages/org/browser-profiles-new.ts b/frontend/src/pages/org/browser-profiles-new.ts index 8dd3326c73..e8c72580f6 100644 --- a/frontend/src/pages/org/browser-profiles-new.ts +++ b/frontend/src/pages/org/browser-profiles-new.ts @@ -182,7 +182,6 @@ export class BrowserProfilesNew extends BtrixElement { (this.isBrowserLoaded = true)} @btrix-browser-reload=${this.onBrowserReload} @btrix-browser-error=${this.onBrowserError} diff --git a/frontend/src/pages/org/browser-profiles/browser.ts b/frontend/src/pages/org/browser-profiles/browser.ts index 145a004522..5a416b4078 100644 --- a/frontend/src/pages/org/browser-profiles/browser.ts +++ b/frontend/src/pages/org/browser-profiles/browser.ts @@ -201,7 +201,6 @@ export class BrowserProfilesBrowserPage extends BtrixElement { (this.isBrowserLoaded = true)} @btrix-browser-reload=${this.onBrowserReload} @btrix-browser-error=${this.onBrowserError} @@ -515,9 +514,12 @@ export class BrowserProfilesBrowserPage extends BtrixElement { ); return data; - } catch (e) { - // TODO Investigate DELETE returning 404 - console.debug(e); + } catch (err) { + if (isApiError(err) && err.statusCode === 404) { + // Safe to ignore, since unloaded browser will have already been deleted + } else { + console.debug(err); + } } } diff --git a/frontend/src/pages/org/browser-profiles/profile.ts b/frontend/src/pages/org/browser-profiles/profile.ts index 4d8ee95310..ad6df691d7 100644 --- a/frontend/src/pages/org/browser-profiles/profile.ts +++ b/frontend/src/pages/org/browser-profiles/profile.ts @@ -379,7 +379,6 @@ export class BrowserProfilesProfilePage extends BtrixElement { ? html` Date: Fri, 7 Nov 2025 13:28:35 -0800 Subject: [PATCH 09/27] configure view --- .../browser-profiles/profile-browser.ts | 101 ++++-- .../profile-metadata-dialog.ts | 4 +- .../profile-settings-dialog.ts | 14 +- .../browser-profiles/templates/badges.ts | 43 +++ .../src/pages/org/browser-profiles/browser.ts | 42 +-- .../src/pages/org/browser-profiles/profile.ts | 330 +++++++----------- frontend/src/pages/org/index.ts | 13 +- frontend/src/utils/pluralize.ts | 26 ++ 8 files changed, 297 insertions(+), 276 deletions(-) create mode 100644 frontend/src/features/browser-profiles/templates/badges.ts diff --git a/frontend/src/features/browser-profiles/profile-browser.ts b/frontend/src/features/browser-profiles/profile-browser.ts index 7e0b2aa0e6..cab11686a5 100644 --- a/frontend/src/features/browser-profiles/profile-browser.ts +++ b/frontend/src/features/browser-profiles/profile-browser.ts @@ -7,6 +7,7 @@ import { cache } from "lit/directives/cache.js"; import { when } from "lit/directives/when.js"; import { BtrixElement } from "@/classes/BtrixElement"; +import { emptyMessage } from "@/layouts/emptyMessage"; import { isApiError, type APIError } from "@/utils/api"; import { tw } from "@/utils/tailwind"; @@ -45,6 +46,8 @@ const isPolling = (value: unknown): value is number => { * @fires btrix-browser-error * @fires btrix-browser-reload * @fires btrix-browser-connection-change + * @cssPart base + * @cssPart browser */ @customElement("btrix-profile-browser") @localized() @@ -59,7 +62,7 @@ export class ProfileBrowser extends BtrixElement { readOnly = false; @property({ type: Boolean }) - disableToggleSites = false; + hideControls = false; @state() private isFullscreen = false; @@ -183,8 +186,6 @@ export class ProfileBrowser extends BtrixElement { return; } - console.log("ping task set timeout"); - return window.setTimeout(() => { if (!signal.aborted) { void this.originsTask.run(); @@ -204,6 +205,8 @@ export class ProfileBrowser extends BtrixElement { if (isPolling(this.browserTask.value)) window.clearTimeout(this.browserTask.value); + window.clearTimeout(this.pingTask.value); + document.removeEventListener("fullscreenchange", this.onFullscreenChange); window.removeEventListener("beforeunload", this.onBeforeUnload); @@ -248,6 +251,10 @@ export class ProfileBrowser extends BtrixElement { } } + public toggleOrigins() { + this.showOriginSidebar = !this.showOriginSidebar; + } + private animateSidebar() { if (!this.sidebar) return; if (this.showOriginSidebar) { @@ -277,14 +284,17 @@ export class ProfileBrowser extends BtrixElement { ); return html` -
    - ${this.renderControlBar()} +
    + ${when(!this.hideControls, this.renderControlBar)}
    ${this.browserTask.render({ initial: browserLoading, @@ -332,6 +342,7 @@ export class ProfileBrowser extends BtrixElement { ), )} `, + () => emptyMessage({ message: msg("No visited sites yet.") }), )}
    @@ -340,11 +351,11 @@ export class ProfileBrowser extends BtrixElement { `; } - private renderControlBar() { + private readonly renderControlBar = () => { if (this.isFullscreen) { return html`
    ${this.renderSidebarButton()} @@ -385,7 +396,7 @@ export class ProfileBrowser extends BtrixElement {
    `; - } + }; private readonly renderBrowser = (browser: BrowserResponseData) => { return html`
    @@ -428,7 +439,7 @@ export class ProfileBrowser extends BtrixElement { (this.showOriginSidebar = !this.showOriginSidebar)} + @click=${() => this.toggleOrigins()} aria-pressed=${this.showOriginSidebar} > @@ -437,20 +448,28 @@ export class ProfileBrowser extends BtrixElement { private renderOrigins(origins: string[]) { return html` -

    - ${msg("Visited Sites")} +
    +

    ${msg("Visited Sites")}

    + +
    + (this.showOriginSidebar = false)} > - -

    + +
      ${origins.map((url) => this.renderOriginItem(url))}
    @@ -461,20 +480,24 @@ export class ProfileBrowser extends BtrixElement { if (!origins.length) return; return html` -

    - ${msg("New Sites")} - -

    +
    +
    +

    ${msg("New Sites")}

    + +
    +
      ${origins.map((url) => this.renderOriginItem(url))}
    @@ -488,7 +511,7 @@ export class ProfileBrowser extends BtrixElement { return html`
  • (iframeSrc ? this.navigateBrowser({ url }) : {})} @@ -501,6 +524,8 @@ export class ProfileBrowser extends BtrixElement { } private onClickReload() { + this.showOriginSidebar = false; + this.dispatchEvent(new CustomEvent("btrix-browser-reload")); } diff --git a/frontend/src/features/browser-profiles/profile-metadata-dialog.ts b/frontend/src/features/browser-profiles/profile-metadata-dialog.ts index 7ba00a9817..23a9aed515 100644 --- a/frontend/src/features/browser-profiles/profile-metadata-dialog.ts +++ b/frontend/src/features/browser-profiles/profile-metadata-dialog.ts @@ -115,7 +115,9 @@ export class ProfileMetadataDialog extends BtrixElement { .label=${msg("Edit Metadata")} .open=${this.open} @sl-show=${() => (this.isDialogVisible = true)} - @sl-after-show=${() => { + @sl-initial-focus=${async () => { + await this.updateComplete; + switch (this.autofocusOn) { case "name": this.nameInput?.focus(); diff --git a/frontend/src/features/browser-profiles/profile-settings-dialog.ts b/frontend/src/features/browser-profiles/profile-settings-dialog.ts index 359a2172b1..abeb7d531c 100644 --- a/frontend/src/features/browser-profiles/profile-settings-dialog.ts +++ b/frontend/src/features/browser-profiles/profile-settings-dialog.ts @@ -61,6 +61,17 @@ export class ProfileSettingsDialog extends BtrixElement { @queryAsync("#browserProfileForm") private readonly form!: Promise; + connectedCallback(): void { + super.connectedCallback(); + + if (this.org?.crawlingDefaults) { + if (!this.defaultProxyId) + this.defaultProxyId = this.org.crawlingDefaults.proxyId; + if (!this.defaultCrawlerChannel) + this.defaultCrawlerChannel = this.org.crawlingDefaults.crawlerChannel; + } + } + protected willUpdate(changedProperties: PropertyValues): void { if (changedProperties.has("defaultProxyId") && this.defaultProxyId) { this.proxyId = this.proxyId || this.defaultProxyId; @@ -85,8 +96,9 @@ export class ProfileSettingsDialog extends BtrixElement { .open=${this.open} @sl-initial-focus=${async (e: CustomEvent) => { const nameInput = (await this.form).querySelector( - 'sl-input[name="url"]', + "btrix-url-input", ); + if (nameInput) { e.preventDefault(); nameInput.focus(); diff --git a/frontend/src/features/browser-profiles/templates/badges.ts b/frontend/src/features/browser-profiles/templates/badges.ts new file mode 100644 index 0000000000..9e47260227 --- /dev/null +++ b/frontend/src/features/browser-profiles/templates/badges.ts @@ -0,0 +1,43 @@ +import { msg } from "@lit/localize"; +import { html, nothing } from "lit"; +import { when } from "lit/directives/when.js"; +import capitalize from "lodash/fp/capitalize"; + +import type { Profile } from "@/types/crawler"; + +export const usageBadge = (inUse: boolean) => + html` + + ${inUse ? msg("In Use") : msg("Not In Use")} + `; + +export const badges = ( + profile: Partial>, +) => { + return html`
    + ${profile.inUse === undefined ? nothing : usageBadge(profile.inUse)} + ${when( + profile.crawlerChannel, + (channel) => + html` + ${capitalize(channel)} ${msg("Channel")}`, + )} + ${when( + profile.proxyId, + (proxy) => + html` + ${proxy} ${msg("Proxy")}`, + )} +
    `; +}; + +export const badgesSkeleton = () => + html``; diff --git a/frontend/src/pages/org/browser-profiles/browser.ts b/frontend/src/pages/org/browser-profiles/browser.ts index 5a416b4078..35c16e319f 100644 --- a/frontend/src/pages/org/browser-profiles/browser.ts +++ b/frontend/src/pages/org/browser-profiles/browser.ts @@ -4,13 +4,16 @@ import { html } from "lit"; import { customElement, property, query, state } from "lit/decorators.js"; import { ifDefined } from "lit/directives/if-defined.js"; import { when } from "lit/directives/when.js"; -import capitalize from "lodash/fp/capitalize"; import queryString from "query-string"; import { BtrixElement } from "@/classes/BtrixElement"; import type { Dialog } from "@/components/ui/dialog"; import type { BtrixUserGuideShowEvent } from "@/events/btrix-user-guide-show"; import type { BrowserConnectionChange } from "@/features/browser-profiles/profile-browser"; +import { + badges, + badgesSkeleton, +} from "@/features/browser-profiles/templates/badges"; import { page } from "@/layouts/page"; import { type Breadcrumb } from "@/layouts/pageHeader"; import { OrgTab } from "@/routes"; @@ -97,35 +100,16 @@ export class BrowserProfilesBrowserPage extends BtrixElement { ]; } - const badges = (profile: { - crawlerChannel?: string; - proxyId?: string | null; - }) => { - return html`
    - ${when( - profile.crawlerChannel, - (channel) => - html` - ${capitalize(channel)} ${msg("Channel")}`, - )} - ${when( - profile.proxyId, - (proxy) => - html` - ${proxy} ${msg("Proxy")}`, - )} -
    `; - }; - const badgesSkeleton = () => - html``; - const header = { breadcrumbs, title: this.profileId ? profile?.name : msg("New Browser Profile"), secondary: when( - this.profileId ? profile : this.config, + this.profileId + ? { + ...profile, + ...this.config, + } + : this.config, badges, badgesSkeleton, ), @@ -364,10 +348,10 @@ export class BrowserProfilesBrowserPage extends BtrixElement { ); } - private async onSubmit(event: SubmitEvent) { - event.preventDefault(); + private async onSubmit(e: SubmitEvent) { + e.preventDefault(); - const formData = new FormData(event.target as HTMLFormElement); + const formData = new FormData(e.target as HTMLFormElement); const params = { name: formData.get("name") as string, description: formData.get("description") as string, diff --git a/frontend/src/pages/org/browser-profiles/profile.ts b/frontend/src/pages/org/browser-profiles/profile.ts index ad6df691d7..b49b5a1be1 100644 --- a/frontend/src/pages/org/browser-profiles/profile.ts +++ b/frontend/src/pages/org/browser-profiles/profile.ts @@ -1,11 +1,11 @@ +import { consume } from "@lit/context"; import { localized, msg } from "@lit/localize"; import { Task } from "@lit/task"; import type { SlMenuItem } from "@shoelace-style/shoelace"; -import { html, type TemplateResult } from "lit"; +import { html, nothing, type TemplateResult } from "lit"; import { customElement, property, query, state } from "lit/decorators.js"; import { ifDefined } from "lit/directives/if-defined.js"; import { when } from "lit/directives/when.js"; -import capitalize from "lodash/fp/capitalize"; import queryString from "query-string"; import { BtrixElement } from "@/classes/BtrixElement"; @@ -14,10 +14,24 @@ import type { FilterChip, } from "@/components/ui/filter-chip"; import { parsePage, type PageChangeEvent } from "@/components/ui/pagination"; -import type { TabGroup } from "@/components/ui/tab-group/tab-group"; +import { + orgCrawlerChannelsContext, + type OrgCrawlerChannelsContext, +} from "@/context/org-crawler-channels"; +import { + orgProxiesContext, + type OrgProxiesContext, +} from "@/context/org-proxies"; import { ClipboardController } from "@/controllers/clipboard"; import { CrawlStatus } from "@/features/archived-items/crawl-status"; -import type { BrowserConnectionChange } from "@/features/browser-profiles/profile-browser"; +import type { + BrowserConnectionChange, + ProfileBrowser, +} from "@/features/browser-profiles/profile-browser"; +import { + badges, + badgesSkeleton, +} from "@/features/browser-profiles/templates/badges"; import type { WorkflowColumnName } from "@/features/crawl-workflows/workflow-list"; import { emptyMessage } from "@/layouts/emptyMessage"; import { labelWithIcon } from "@/layouts/labelWithIcon"; @@ -38,11 +52,6 @@ import { tw } from "@/utils/tailwind"; const INITIAL_PAGE_SIZE = 5; const WORKFLOW_PAGE_QUERY = "workflowsPage"; -enum ProfileTab { - Config = "config", - Browser = "browser", -} - const workflowColumns = [ "name", "latest-crawl", @@ -52,6 +61,12 @@ const workflowColumns = [ @customElement("btrix-browser-profiles-profile-page") @localized() export class BrowserProfilesProfilePage extends BtrixElement { + @consume({ context: orgProxiesContext, subscribe: true }) + private readonly orgProxies?: OrgProxiesContext; + + @consume({ context: orgCrawlerChannelsContext, subscribe: true }) + private readonly orgCrawlerChannels?: OrgCrawlerChannelsContext; + @property({ type: String }) profileId = ""; @@ -70,13 +85,8 @@ export class BrowserProfilesProfilePage extends BtrixElement { | "metadata" | "metadata-name" | "metadata-description" - | "config"; - - @state() - private configuringProfile = false; - - @state() - private activetab = ProfileTab.Config; + | "config" + | "browser"; @state() private initialNavigateUrl?: string; @@ -84,8 +94,8 @@ export class BrowserProfilesProfilePage extends BtrixElement { @state() private isBrowserLoaded = false; - @query("btrix-tab-group") - private readonly tabGroup?: TabGroup | null; + @query("btrix-profile-browser") + private readonly profileBrowser?: ProfileBrowser | null; private get profile() { return this.profileTask.value; @@ -123,38 +133,6 @@ export class BrowserProfilesProfilePage extends BtrixElement { }); render() { - const badges = (profile: Profile) => { - return html`
    - - - ${profile.inUse ? msg("In Use") : msg("Not In Use")} - - - ${when( - profile.crawlerChannel, - (channel) => - html` - ${capitalize(channel)} ${msg("Channel")}`, - )} - ${when( - profile.proxyId, - (proxy) => - html` - ${proxy} ${msg("Proxy")}`, - )} -
    `; - }; - const badgesSkeleton = () => - html``; - const header = { breadcrumbs: [ { @@ -181,29 +159,15 @@ export class BrowserProfilesProfilePage extends BtrixElement { > `, )} `, - // prefix: when( - // this.profile, - // (profile) => - // html` - // - // `, - // () => - // html``, - // ), secondary: when(this.profile, badges, badgesSkeleton), actions: this.renderActions(), } satisfies Parameters[0]; + const org = this.org; + const proxies = this.orgProxies; + const crawlerChannels = this.orgCrawlerChannels; + const crawlingDefaultsReady = org && proxies && crawlerChannels; + return html`${page(header, this.renderPage)} ${when( this.profile, @@ -226,23 +190,20 @@ export class BrowserProfilesProfilePage extends BtrixElement { > - (this.openDialog = undefined)} - @btrix-submit=${async () => { - this.openDialog = undefined; - this.activetab = ProfileTab.Browser; - this.openBrowser(); - - await this.updateComplete; - - this.configuringProfile = true; - }} - > `, + ${crawlingDefaultsReady + ? html` (this.openDialog = undefined)} + >` + : nothing} `, )} `; } @@ -255,6 +216,10 @@ export class BrowserProfilesProfilePage extends BtrixElement { }; return html` + this.openBrowser()}> + + ${msg("View Profile")} + ${msg("Actions")} @@ -314,7 +279,7 @@ export class BrowserProfilesProfilePage extends BtrixElement { private readonly renderPage = () => { return html` -
    +
    ${this.renderProfile()} ${this.renderUsage()}
    @@ -327,93 +292,57 @@ export class BrowserProfilesProfilePage extends BtrixElement { }; private renderProfile() { - const showBrowser = - this.activetab === ProfileTab.Browser && - this.profileTask.value && - this.browserIdTask.value && - this.initialNavigateUrl; - return html` - ) => { - this.activetab = e.detail as ProfileTab; - - if (e.detail === (ProfileTab.Browser as string)) { - this.openBrowser(); - } else { - this.initialNavigateUrl = ""; - } - }} - > - - - ${msg("Visited Sites")} - - + (this.openDialog = "config")} + > + ` + : undefined, + body: html`${this.renderOrigins()} + + this.closeBrowser()} > - - ${msg("Inspect Profile")} - - - ${this.renderOrigins()} - - ${when( - !this.configuringProfile, - () => html` - - (this.openDialog = "config")} - > - - `, - )} - -
    - ${showBrowser - ? html` ` - : html`
    -
    `} + this.profileBrowser?.toggleOrigins()} + > + ${readyBrowserId + ? html` ` + : html`
    `} +
    + ${msg("View Only")} + ${msg("Browsing history will not be saved to profile.")}
    - - ${when(this.configuringProfile, this.renderProfileControls)} - - - `; + `, + }); } - private readonly renderProfileControls = () => { - return html` -
    - (this.configuringProfile = false)} - > - ${msg("Cancel")} - -
    - - ${msg("Save Profile")} - -
    -
    - `; - }; private renderOrigins() { const originsSkeleton = () => html`
    `; @@ -421,30 +350,32 @@ export class BrowserProfilesProfilePage extends BtrixElement { const origins = (profile: Profile) => profile.origins.map( (origin) => html` -
  • - - - -
    - - +
  • +
    +
    + + +
    + +
    + +
    + + { + this.initialNavigateUrl = origin; + this.openBrowser(); + }} + > + ${this.renderDetail( (profile) => - `${this.localize.number(profile.origins.length)} ${pluralOf("URLs", profile.origins.length)}`, + `${this.localize.number(profile.origins.length)} ${pluralOf("origins", profile.origins.length)}`, )} @@ -721,7 +652,12 @@ export class BrowserProfilesProfilePage extends BtrixElement { void this.browserIdTask.run(); - this.tabGroup?.scrollIntoView(); + this.openDialog = "browser"; + }; + + private readonly closeBrowser = () => { + this.initialNavigateUrl = undefined; + this.openDialog = undefined; }; private readonly onBrowserLoad = () => { diff --git a/frontend/src/pages/org/index.ts b/frontend/src/pages/org/index.ts index 71c1f89945..6b033d6030 100644 --- a/frontend/src/pages/org/index.ts +++ b/frontend/src/pages/org/index.ts @@ -466,7 +466,7 @@ export class Org extends BtrixElement { const org = this.org; const proxies = this.proxiesTask.value; const crawlerChannels = this.crawlerChannelsTask.value; - const showBrowserProfileDialog = org && proxies && crawlerChannels; + const crawlingDefaultsReady = org && proxies && crawlerChannels; return html`
    - ${showBrowserProfileDialog + ${crawlingDefaultsReady ? html` (this.openDialogName = undefined)} > diff --git a/frontend/src/utils/pluralize.ts b/frontend/src/utils/pluralize.ts index 943ebb7df4..4abcc13b5f 100644 --- a/frontend/src/utils/pluralize.ts +++ b/frontend/src/utils/pluralize.ts @@ -325,6 +325,32 @@ const plurals = { id: "workflows.plural.other", }), }, + origins: { + zero: msg("origins", { + desc: 'plural form of "origin" for zero origins', + id: "origins.plural.zero", + }), + one: msg("origin", { + desc: 'singular form for "origin"', + id: "origins.plural.one", + }), + two: msg("origins", { + desc: 'plural form of "origin" for two origins', + id: "origins.plural.two", + }), + few: msg("origins", { + desc: 'plural form of "origin" for few origins', + id: "origins.plural.few", + }), + many: msg("origins", { + desc: 'plural form of "origin" for many origins', + id: "origins.plural.many", + }), + other: msg("origins", { + desc: 'plural form of "origin" for multiple/other origins', + id: "origins.plural.other", + }), + }, }; export const pluralOf = (word: keyof typeof plurals, count: number) => { From 9131b550feba97d8e1e2f0b6679e63b7c2fde02c Mon Sep 17 00:00:00 2001 From: sua yoo Date: Fri, 7 Nov 2025 13:30:17 -0800 Subject: [PATCH 10/27] configure view --- .../profile-settings-dialog.ts | 1 - .../src/pages/org/browser-profiles/profile.ts | 2 +- frontend/src/utils/pluralize.ts | 48 +++++++++---------- 3 files changed, 25 insertions(+), 26 deletions(-) diff --git a/frontend/src/features/browser-profiles/profile-settings-dialog.ts b/frontend/src/features/browser-profiles/profile-settings-dialog.ts index abeb7d531c..9d9124e4de 100644 --- a/frontend/src/features/browser-profiles/profile-settings-dialog.ts +++ b/frontend/src/features/browser-profiles/profile-settings-dialog.ts @@ -98,7 +98,6 @@ export class ProfileSettingsDialog extends BtrixElement { const nameInput = (await this.form).querySelector( "btrix-url-input", ); - if (nameInput) { e.preventDefault(); nameInput.focus(); diff --git a/frontend/src/pages/org/browser-profiles/profile.ts b/frontend/src/pages/org/browser-profiles/profile.ts index b49b5a1be1..8c189c4b17 100644 --- a/frontend/src/pages/org/browser-profiles/profile.ts +++ b/frontend/src/pages/org/browser-profiles/profile.ts @@ -443,7 +443,7 @@ export class BrowserProfilesProfilePage extends BtrixElement { ${this.renderDetail( (profile) => - `${this.localize.number(profile.origins.length)} ${pluralOf("origins", profile.origins.length)}`, + `${this.localize.number(profile.origins.length)} ${pluralOf("domains", profile.origins.length)}`, )} diff --git a/frontend/src/utils/pluralize.ts b/frontend/src/utils/pluralize.ts index 4abcc13b5f..83afe273c2 100644 --- a/frontend/src/utils/pluralize.ts +++ b/frontend/src/utils/pluralize.ts @@ -325,30 +325,30 @@ const plurals = { id: "workflows.plural.other", }), }, - origins: { - zero: msg("origins", { - desc: 'plural form of "origin" for zero origins', - id: "origins.plural.zero", - }), - one: msg("origin", { - desc: 'singular form for "origin"', - id: "origins.plural.one", - }), - two: msg("origins", { - desc: 'plural form of "origin" for two origins', - id: "origins.plural.two", - }), - few: msg("origins", { - desc: 'plural form of "origin" for few origins', - id: "origins.plural.few", - }), - many: msg("origins", { - desc: 'plural form of "origin" for many origins', - id: "origins.plural.many", - }), - other: msg("origins", { - desc: 'plural form of "origin" for multiple/other origins', - id: "origins.plural.other", + domains: { + zero: msg("domains", { + desc: 'plural form of "domain" for zero domains', + id: "domains.plural.zero", + }), + one: msg("domain", { + desc: 'singular form for "domain"', + id: "domains.plural.one", + }), + two: msg("domains", { + desc: 'plural form of "domain" for two domains', + id: "domains.plural.two", + }), + few: msg("domains", { + desc: 'plural form of "domain" for few domains', + id: "domains.plural.few", + }), + many: msg("domains", { + desc: 'plural form of "domain" for many domains', + id: "domains.plural.many", + }), + other: msg("domains", { + desc: 'plural form of "domain" for multiple/other domains', + id: "domains.plural.other", }), }, }; From bceaee11757f8c20b46646bc1832005181fdeb3c Mon Sep 17 00:00:00 2001 From: sua yoo Date: Fri, 7 Nov 2025 13:39:15 -0800 Subject: [PATCH 11/27] create new with profile --- .../src/pages/org/browser-profiles/profile.ts | 26 ++++++++++++++++++- .../crawl-workflows/settingsForDuplicate.ts | 8 +++--- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/frontend/src/pages/org/browser-profiles/profile.ts b/frontend/src/pages/org/browser-profiles/profile.ts index 8c189c4b17..aadab0a2fa 100644 --- a/frontend/src/pages/org/browser-profiles/profile.ts +++ b/frontend/src/pages/org/browser-profiles/profile.ts @@ -44,6 +44,7 @@ import type { Profile, Workflow } from "@/types/crawler"; import type { CrawlState } from "@/types/crawlState"; import { SortDirection } from "@/types/utils"; import { isApiError } from "@/utils/api"; +import { settingsForDuplicate } from "@/utils/crawl-workflows/settingsForDuplicate"; import { isNotEqual } from "@/utils/is-not-equal"; import { isArchivingDisabled } from "@/utils/orgs"; import { pluralOf } from "@/utils/pluralize"; @@ -520,7 +521,30 @@ export class BrowserProfilesProfilePage extends BtrixElement { message: msg( "This profile is not in use by any crawl workflows.", ), - actions: html` + actions: html` { + this.navigate.to( + `${this.navigate.orgBasePath}/${OrgTab.Workflows}/new`, + settingsForDuplicate({ + workflow: { + profileid: this.profileId, + proxyId: profile.proxyId, + crawlerChannel: profile.crawlerChannel, + }, + }), + ); + + this.notify.toast({ + message: msg( + "Copied browser settings to new workflow.", + ), + variant: "success", + icon: "check2-circle", + id: "workflow-copied-status", + }); + }} + > ${msg("Create Workflow Using Profile")}`, diff --git a/frontend/src/utils/crawl-workflows/settingsForDuplicate.ts b/frontend/src/utils/crawl-workflows/settingsForDuplicate.ts index f664f62845..5ecf3b9247 100644 --- a/frontend/src/utils/crawl-workflows/settingsForDuplicate.ts +++ b/frontend/src/utils/crawl-workflows/settingsForDuplicate.ts @@ -16,7 +16,7 @@ import { } from "@/types/workflow"; export type DuplicateWorkflowSettings = { - workflow: WorkflowParams; + workflow: Partial; scopeType?: ScopeType | NewWorkflowOnlyScopeType; seeds?: Seed[]; seedFile?: StorageSeedFile; @@ -27,11 +27,11 @@ export function settingsForDuplicate({ seeds, seedFile, }: { - workflow: Workflow; + workflow: Partial; seeds?: APIPaginatedList; seedFile?: StorageSeedFile; }): DuplicateWorkflowSettings { - const workflowParams: WorkflowParams = { + const workflowParams: Partial = { ...workflow, name: workflow.name ? msg(str`${workflow.name} Copy`) : "", }; @@ -42,7 +42,7 @@ export function settingsForDuplicate({ scopeType: seedFile || (seedItems?.length && seedItems.length > 1) ? NewWorkflowOnlyScopeType.PageList - : workflowParams.config.scopeType, + : workflowParams.config?.scopeType, workflow: workflowParams, seeds: seedItems, seedFile, From 19194b5c465b90180d25d67db3cf0e36fda1f275 Mon Sep 17 00:00:00 2001 From: sua yoo Date: Fri, 7 Nov 2025 13:53:47 -0800 Subject: [PATCH 12/27] update badges --- .../profile-metadata-dialog.ts | 2 +- .../browser-profiles/templates/badges.ts | 25 +++++++++++++------ .../collections/collection-edit-dialog.ts | 2 +- .../collections/select-collection-access.ts | 2 +- frontend/src/pages/org/dashboard.ts | 2 +- frontend/src/pages/public/org.ts | 2 +- 6 files changed, 23 insertions(+), 12 deletions(-) diff --git a/frontend/src/features/browser-profiles/profile-metadata-dialog.ts b/frontend/src/features/browser-profiles/profile-metadata-dialog.ts index 23a9aed515..dd68e0f0ee 100644 --- a/frontend/src/features/browser-profiles/profile-metadata-dialog.ts +++ b/frontend/src/features/browser-profiles/profile-metadata-dialog.ts @@ -79,7 +79,7 @@ export class ProfileMetadataDialog extends BtrixElement { ); this.notify.toast({ - message: msg("Browser profile metadata updated."), + message: msg("Updated browser profile metadata."), variant: "success", icon: "check2-circle", id: "browser-profile-save-status", diff --git a/frontend/src/features/browser-profiles/templates/badges.ts b/frontend/src/features/browser-profiles/templates/badges.ts index 9e47260227..3c7c824928 100644 --- a/frontend/src/features/browser-profiles/templates/badges.ts +++ b/frontend/src/features/browser-profiles/templates/badges.ts @@ -3,7 +3,7 @@ import { html, nothing } from "lit"; import { when } from "lit/directives/when.js"; import capitalize from "lodash/fp/capitalize"; -import type { Profile } from "@/types/crawler"; +import { CrawlerChannelImage, type Profile } from "@/types/crawler"; export const usageBadge = (inUse: boolean) => html` - html` - ${capitalize(channel)} ${msg("Channel")}`, + html` + + + ${capitalize(channel)} + + `, )} ${when( profile.proxyId, (proxy) => - html` - ${proxy} ${msg("Proxy")}`, + html` + + + ${proxy} + + `, )}
    `; }; diff --git a/frontend/src/features/collections/collection-edit-dialog.ts b/frontend/src/features/collections/collection-edit-dialog.ts index fed4f33913..8884f31b08 100644 --- a/frontend/src/features/collections/collection-edit-dialog.ts +++ b/frontend/src/features/collections/collection-edit-dialog.ts @@ -249,7 +249,7 @@ export class CollectionEdit extends BtrixElement { })} ${this.renderTab({ panel: "sharing", - icon: "globe2", + icon: "globe", string: msg("Sharing"), })} diff --git a/frontend/src/features/collections/select-collection-access.ts b/frontend/src/features/collections/select-collection-access.ts index 2e8c9d5df9..2065ac0c4a 100644 --- a/frontend/src/features/collections/select-collection-access.ts +++ b/frontend/src/features/collections/select-collection-access.ts @@ -27,7 +27,7 @@ export class SelectCollectionAccess extends BtrixElement { }, [CollectionAccess.Public]: { label: msg("Public"), - icon: "globe2", + icon: "globe", detail: msg("Anyone can view from the org public collections gallery"), }, }; diff --git a/frontend/src/pages/org/dashboard.ts b/frontend/src/pages/org/dashboard.ts index 170e214e9e..1780b0541c 100644 --- a/frontend/src/pages/org/dashboard.ts +++ b/frontend/src/pages/org/dashboard.ts @@ -160,7 +160,7 @@ export class Dashboard extends BtrixElement { class="flex items-center gap-1.5 text-pretty text-neutral-700" > diff --git a/frontend/src/pages/public/org.ts b/frontend/src/pages/public/org.ts index 91a2f122e7..a86912418b 100644 --- a/frontend/src/pages/public/org.ts +++ b/frontend/src/pages/public/org.ts @@ -176,7 +176,7 @@ export class PublicOrg extends BtrixElement { class="flex items-center gap-1.5 text-pretty text-neutral-700" > From cc81502e9f6a87a5fe5727763c3b53da4fda8ead Mon Sep 17 00:00:00 2001 From: sua yoo Date: Fri, 7 Nov 2025 16:29:16 -0800 Subject: [PATCH 13/27] delete unused files --- .../src/pages/org/browser-profiles-detail.ts | 804 ------------------ .../src/pages/org/browser-profiles-new.ts | 501 ----------- frontend/src/pages/org/index.ts | 1 - 3 files changed, 1306 deletions(-) delete mode 100644 frontend/src/pages/org/browser-profiles-detail.ts delete mode 100644 frontend/src/pages/org/browser-profiles-new.ts diff --git a/frontend/src/pages/org/browser-profiles-detail.ts b/frontend/src/pages/org/browser-profiles-detail.ts deleted file mode 100644 index 01fa76cc82..0000000000 --- a/frontend/src/pages/org/browser-profiles-detail.ts +++ /dev/null @@ -1,804 +0,0 @@ -import { localized, msg, str } from "@lit/localize"; -import { html, nothing } from "lit"; -import { customElement, property, query, state } from "lit/decorators.js"; -import { ifDefined } from "lit/directives/if-defined.js"; -import { when } from "lit/directives/when.js"; -import capitalize from "lodash/fp/capitalize"; -import queryString from "query-string"; - -import type { Profile } from "./types"; - -import { BtrixElement } from "@/classes/BtrixElement"; -import type { Dialog } from "@/components/ui/dialog"; -import { ClipboardController } from "@/controllers/clipboard"; -import type { BrowserConnectionChange } from "@/features/browser-profiles/profile-browser"; -import { pageNav } from "@/layouts/pageHeader"; -import { isApiError } from "@/utils/api"; -import { maxLengthValidator } from "@/utils/form"; -import { isArchivingDisabled } from "@/utils/orgs"; -import { richText } from "@/utils/rich-text"; - -const DESCRIPTION_MAXLENGTH = 500; - -/** - * Usage: - * ```ts - * - * ``` - */ -@customElement("btrix-browser-profiles-detail") -@localized() -export class BrowserProfilesDetail extends BtrixElement { - @property({ type: String }) - profileId!: string; - - @property({ type: Boolean }) - isCrawler = false; - - @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; - - @query("#profileBrowserContainer") - private readonly profileBrowserContainer?: HTMLElement | null; - - @query("#discardChangesDialog") - private readonly discardChangesDialog?: Dialog | null; - - private readonly validateNameMax = maxLengthValidator(50); - private readonly validateDescriptionMax = maxLengthValidator(500); - - private updatedProfileTimer?: number; - - disconnectedCallback() { - window.clearTimeout(this.updatedProfileTimer); - if (this.browserId) { - void this.deleteBrowser(this.browserId); - } - super.disconnectedCallback(); - } - - firstUpdated() { - void this.fetchProfile(); - } - - render() { - const isBackedUp = - this.profile?.resource?.replicas && - this.profile.resource.replicas.length > 0; - const none = html`${msg("None")}`; - - return html`
    ${this.renderBreadcrumbs()}
    - -
    -

    - ${this.profile?.name - ? html`${this.profile.name}` - : html``} -

    -
    - ${this.profile - ? this.renderMenu() - : html``} -
    -
    - -
    - - - ${this.profile - ? this.profile.crawlerChannel - ? capitalize(this.profile.crawlerChannel) - : none - : nothing} - - - ${this.profile - ? html` - - ` - : nothing} - - - ${this.profile - ? html` ` - : nothing} - - ${ - // NOTE older profiles may not have "modified/created by" data - this.profile?.modifiedByName || this.profile?.createdByName - ? html` - - ${this.profile.modifiedByName || this.profile.createdByName} - - ` - : nothing - } - -
    - -
    -
    -
    -

    - ${msg("Browser Profile")} -

    - - - - - ${this.profile?.inUse - ? html` - - - ${msg("In Use")} - - - - ` - : html` - - `} -
    - - ${when(this.isCrawler, () => - this.browserId || this.isBrowserLoading - ? html` -
    -
    - - (this.isBrowserLoaded = true)} - @btrix-browser-error=${this.onBrowserError} - @btrix-browser-reload=${this.startBrowserPreview} - @btrix-browser-connection-change=${this - .onBrowserConnectionChange} - > -
    - ${this.renderBrowserProfileControls()} -
    -
    -
    - ` - : html`
    -

    - ${msg( - "View or edit the current browser profile configuration.", - )} -

    - - - ${msg("Configure Browser Profile")} - -
    `, - )} -
    - ${when( - !(this.browserId || this.isBrowserLoading), - this.renderVisitedSites, - )} -
    - -
    -
    -

    - ${msg("Description")} -

    - ${when( - this.isCrawler, - () => html` - (this.isEditDialogOpen = true)} - label=${msg("Edit description")} - > - `, - )} -
    - -
    ${this.profile - ? this.profile.description - ? richText(this.profile.description) - : html` -
    -  ${msg("No description added.")} -
    - ` - : nothing}
    -
    - - - ${msg( - "Are you sure you want to discard changes to this browser profile?", - )} -
    - void this.discardChangesDialog?.hide()} - > - ${msg("No, Continue Editing")} - - { - void this.cancelEditBrowser(); - void this.discardChangesDialog?.hide(); - }} - >${msg("Yes, Discard Changes")} - -
    -
    - - (this.isEditDialogOpen = false)} - @sl-show=${() => (this.isEditDialogContentVisible = true)} - @sl-after-hide=${() => (this.isEditDialogContentVisible = false)} - > - ${this.isEditDialogContentVisible ? this.renderEditProfile() : nothing} - `; - } - - private renderBreadcrumbs() { - const breadcrumbs = [ - { - href: `${this.navigate.orgBasePath}/browser-profiles`, - content: msg("Browser Profiles"), - }, - { - content: this.profile?.name, - }, - ]; - - return pageNav(breadcrumbs); - } - - private readonly renderVisitedSites = () => { - return html` -
    -
    -

    - ${msg("Visited Sites")} -

    -
    -
    -
      - ${this.profile?.origins.map((origin) => html`
    • ${origin}
    • `)} -
    -
    -
    - `; - }; - - private renderBrowserProfileControls() { - return html` -
    - - ${msg("Cancel")} - -
    - - ${msg("Save Browser Profile")} - -
    -
    - `; - } - - private renderMenu() { - const archivingDisabled = isArchivingDisabled(this.org); - - return html` - - - ${msg("Actions")} - - - (this.isEditDialogOpen = true)}> - - ${msg("Edit Metadata")} - - - - ${msg("Configure Browser Profile")} - - void this.duplicateProfile()} - > - - ${msg("Duplicate Profile")} - - - ClipboardController.copyToClipboard(this.profileId)} - > - - ${msg("Copy Profile ID")} - - - void this.deleteProfile()} - > - - ${msg("Delete Profile")} - - - - `; - } - - private renderEditProfile() { - if (!this.profile) return; - - return html` -
    -
    - -
    - -
    - -
    - -
    - (this.isEditDialogOpen = false)} - >${msg("Cancel")} - ${msg("Save Changes")} -
    -
    - `; - } - - private async onBrowserError() { - this.isBrowserLoaded = false; - } - - private async onBrowserConnectionChange( - e: CustomEvent, - ) { - this.isBrowserLoaded = e.detail.connected; - } - - private async onCancel() { - if (this.isBrowserLoaded) { - void this.discardChangesDialog?.show(); - } else { - void this.cancelEditBrowser(); - } - } - - 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; - - await this.updateComplete; - - this.profileBrowserContainer?.scrollIntoView({ behavior: "smooth" }); - } catch (e) { - this.isBrowserLoading = false; - - this.notify.toast({ - message: msg("Sorry, couldn't preview browser profile at this time."), - variant: "danger", - icon: "exclamation-octagon", - id: "browser-profile-error", - }); - } - } - - 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.toast({ - message: msg("Starting up browser with current profile..."), - variant: "success", - icon: "check2-circle", - }); - - this.navigate.to( - `${this.navigate.orgBasePath}/browser-profiles/profile/browser/${ - data.browserid - }?${queryString.stringify({ - url, - name: this.profile.name, - description: this.profile.description.slice(0, DESCRIPTION_MAXLENGTH), - profileId: this.profile.id, - crawlerChannel: this.profile.crawlerChannel, - proxyId: this.profile.proxyId, - })}`, - ); - } catch (e) { - this.isBrowserLoading = false; - - this.notify.toast({ - message: msg("Sorry, couldn't create browser profile at this time."), - variant: "danger", - icon: "exclamation-octagon", - id: "browser-profile-error", - }); - } - } - - private async deleteProfile() { - const profileName = this.profile!.name; - - try { - await this.api.fetch( - `/orgs/${this.orgId}/profiles/${this.profile!.id}`, - { - method: "DELETE", - }, - ); - - this.navigate.to(`${this.navigate.orgBasePath}/browser-profiles`); - - this.notify.toast({ - message: msg(html`Deleted ${profileName}.`), - variant: "success", - icon: "check2-circle", - }); - } catch (e) { - let message = msg( - html`Sorry, couldn't delete browser profile at this time.`, - ); - - if (isApiError(e)) { - if (e.message === "profile_in_use") { - message = msg( - html`Could not delete ${profileName}, currently in - use. Please remove browser profile from all crawl workflows to - continue.`, - ); - } - } - this.notify.toast({ - message: message, - variant: "danger", - icon: "exclamation-octagon", - id: "browser-profile-error", - }); - } - } - - private async createBrowser({ url }: { url: string }) { - const params = { - url, - profileId: this.profile!.id, - }; - - return this.api.fetch<{ browserid: string }>( - `/orgs/${this.orgId}/profiles/browser`, - { - method: "POST", - body: JSON.stringify(params), - }, - ); - } - - private async deleteBrowser(id: string) { - return this.api.fetch(`/orgs/${this.orgId}/profiles/browser/${id}`, { - 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.toast({ - message: msg("Sorry, couldn't retrieve browser profiles at this time."), - variant: "danger", - icon: "exclamation-octagon", - id: "browser-profile-error", - }); - } - } - - private async getProfile() { - const data = await this.api.fetch( - `/orgs/${this.orgId}/profiles/${this.profileId}`, - ); - - return data; - } - - private async saveBrowser() { - if (!this.browserId) return; - - this.isSubmittingBrowserChange = true; - - const params = { - name: this.profile!.name, - browserid: this.browserId, - }; - - try { - let retriesLeft = 300; - - while (retriesLeft > 0) { - const data = await this.api.fetch<{ - updated?: boolean; - detail?: string; - }>(`/orgs/${this.orgId}/profiles/${this.profileId}`, { - method: "PATCH", - body: JSON.stringify(params), - }); - if (data.updated !== undefined) { - break; - } - if (data.detail === "waiting_for_browser") { - await new Promise((resolve) => { - this.updatedProfileTimer = window.setTimeout(resolve, 2000); - }); - } else { - throw new Error("unknown response"); - } - - retriesLeft -= 1; - } - - if (!retriesLeft) { - throw new Error("too many retries waiting for browser"); - } - - this.notify.toast({ - message: msg("Successfully saved browser profile."), - variant: "success", - icon: "check2-circle", - id: "browser-profile-save-status", - }); - - this.browserId = undefined; - } catch (e) { - console.debug(e); - - this.notify.toast({ - message: msg("Sorry, couldn't save browser profile at this time."), - variant: "danger", - icon: "exclamation-octagon", - id: "browser-profile-save-status", - }); - } - - this.isSubmittingBrowserChange = false; - } - - private async onSubmitEdit(e: SubmitEvent) { - e.preventDefault(); - - const formEl = e.target as HTMLFormElement; - if (!(await this.checkFormValidity(formEl))) return; - - const formData = new FormData(formEl); - const name = formData.get("name") as string; - const description = formData.get("description") as string; - - const params = { - name, - description, - }; - - this.isSubmittingProfileChange = true; - - try { - const data = await this.api.fetch<{ updated: boolean }>( - `/orgs/${this.orgId}/profiles/${this.profileId}`, - { - method: "PATCH", - body: JSON.stringify(params), - }, - ); - - if (data.updated) { - this.notify.toast({ - message: msg("Successfully saved browser profile."), - variant: "success", - icon: "check2-circle", - id: "browser-profile-save-status", - }); - - 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.toast({ - message: message, - variant: "danger", - icon: "exclamation-octagon", - id: "browser-profile-save-status", - }); - } - - this.isSubmittingProfileChange = false; - } - - async checkFormValidity(formEl: HTMLFormElement) { - await this.updateComplete; - return !formEl.querySelector("[data-invalid]"); - } -} diff --git a/frontend/src/pages/org/browser-profiles-new.ts b/frontend/src/pages/org/browser-profiles-new.ts deleted file mode 100644 index e8c72580f6..0000000000 --- a/frontend/src/pages/org/browser-profiles-new.ts +++ /dev/null @@ -1,501 +0,0 @@ -import { localized, msg, str } from "@lit/localize"; -import { html } from "lit"; -import { customElement, property, query, state } from "lit/decorators.js"; -import { ifDefined } from "lit/directives/if-defined.js"; -import { when } from "lit/directives/when.js"; -import capitalize from "lodash/fp/capitalize"; -import queryString from "query-string"; - -import { BtrixElement } from "@/classes/BtrixElement"; -import type { Dialog } from "@/components/ui/dialog"; -import type { BtrixUserGuideShowEvent } from "@/events/btrix-user-guide-show"; -import type { BrowserConnectionChange } from "@/features/browser-profiles/profile-browser"; -import { page } from "@/layouts/page"; -import { type Breadcrumb } from "@/layouts/pageHeader"; -import { OrgTab } from "@/routes"; -import { CrawlerChannelImage } from "@/types/crawler"; -import { isApiError } from "@/utils/api"; - -/** - * Usage: - * ```ts - * - * ``` - */ -@customElement("btrix-browser-profiles-new") -@localized() -export class BrowserProfilesNew extends BtrixElement { - @property({ type: String }) - browserId!: string; - - @property({ type: Object, attribute: false }) - browserParams: { - url: string; - name?: string; - origins?: string[]; - crawlerChannel?: string; - profileId?: string; - proxyId?: string; - } = { - url: "", - name: "", - }; - - @state() - private isSubmitting = false; - - @state() - private isDialogVisible = false; - - @state() - private isBrowserLoaded = false; - - @query("#discardDialog") - private readonly discardDialog?: Dialog | null; - - disconnectedCallback(): void { - void this.closeBrowser(); - super.disconnectedCallback(); - } - - render() { - const { profileId, name, origins } = this.browserParams; - - let breadcrumbs: Breadcrumb[] = [ - { - href: `${this.navigate.orgBasePath}/${OrgTab.BrowserProfiles}`, - content: msg("Browser Profiles"), - }, - ]; - if (profileId && name) { - breadcrumbs = [ - ...breadcrumbs, - { - href: `${this.navigate.orgBasePath}/${OrgTab.BrowserProfiles}/profile/${profileId}`, - content: name, - }, - { - content: origins - ? msg("Configure Profile") - : msg("Duplicate Profile"), - }, - ]; - } - - const badges = (profile: { - crawlerChannel?: string; - proxyId?: string | null; - }) => { - return html`
    - ${when( - profile.crawlerChannel, - (channel) => - html` - ${capitalize(channel)} ${msg("Channel")}`, - )} - ${when( - profile.proxyId, - (proxy) => - html` - ${proxy} ${msg("Proxy")}`, - )} -
    `; - }; - - const header = { - breadcrumbs, - title: - profileId && name - ? origins - ? name - : msg(str`Configure Duplicate of ${name}`) - : msg("New Browser Profile"), - secondary: badges(this.browserParams), - actions: html` { - this.dispatchEvent( - new CustomEvent( - "btrix-user-guide-show", - { - detail: { path: "browser-profiles" }, - bubbles: true, - composed: true, - }, - ), - ); - }} - > - - ${msg("User Guide")} - `, - border: false, - } satisfies Parameters[0]; - - return html` - ${page(header, this.renderPage)} - - (this.isDialogVisible = false)} - > - ${this.renderForm()} - - - - ${msg( - "Are you sure you want to discard changes to this browser profile?", - )} -
    - void this.discardDialog?.hide()} - > - ${msg("No, Continue Browsing")} - - { - void this.discardDialog?.hide(); - void this.closeBrowser(); - }} - >${msg("Yes, Cancel")} - -
    -
    - `; - } - - private readonly renderPage = () => { - return html`
    - (this.isBrowserLoaded = true)} - @btrix-browser-reload=${this.onBrowserReload} - @btrix-browser-error=${this.onBrowserError} - @btrix-browser-connection-change=${this.onBrowserConnectionChange} - > -
    - -
    - ${this.renderBrowserProfileControls()} -
    `; - }; - - private async onBrowserError() { - this.isBrowserLoaded = false; - } - - private async onBrowserConnectionChange( - e: CustomEvent, - ) { - this.isBrowserLoaded = e.detail.connected; - } - - private onCancel() { - if (!this.isBrowserLoaded) { - void this.closeBrowser(); - } else { - void this.discardDialog?.show(); - } - } - - private async closeBrowser() { - this.isBrowserLoaded = false; - - if (this.browserId) { - await this.deleteBrowser(this.browserId); - } - this.navigate.to(`${this.navigate.orgBasePath}/browser-profiles`); - } - - private renderBrowserProfileControls() { - const { profileId, name, origins } = this.browserParams; - const shouldSave = profileId && name && origins; - - return html` -
    - - ${msg("Cancel")} - -
    - { - if (shouldSave) { - void this.saveProfile({ name }); - } else { - this.isDialogVisible = true; - } - }} - > - ${msg(shouldSave ? "Save Profile" : "Finish Browsing")} - -
    -
    - `; - } - - private renderForm() { - const { profileId, name, origins } = this.browserParams; - const nameValue = - profileId && name - ? // Updating profile - origins - ? name - : // Duplicating profile - `${name} ${msg("Copy")}` - : // New profile - ""; - - return html`
    -
    - - - - -
    - (this.isDialogVisible = false)} - > - ${msg("Back")} - - - - ${msg("Save Profile")} - -
    -
    -
    `; - } - - private async onBrowserReload() { - const { url } = this.browserParams; - if (!url) { - console.debug("no start url"); - return; - } - - const crawlerChannel = - this.browserParams.crawlerChannel || CrawlerChannelImage.Default; - const proxyId = this.browserParams.proxyId ?? null; - const profileId = this.browserParams.profileId || undefined; - const data = await this.createBrowser({ - url, - crawlerChannel, - proxyId, - profileId, - }); - - this.navigate.to( - `${this.navigate.orgBasePath}/browser-profiles/profile/browser/${ - data.browserid - }?${queryString.stringify({ - url, - name: this.browserParams.name, - crawlerChannel, - proxyId, - profileId, - })}`, - ); - } - - private async onSubmit(event: SubmitEvent) { - event.preventDefault(); - - const formData = new FormData(event.target as HTMLFormElement); - const params = { - name: formData.get("name") as string, - description: formData.get("description") as string, - crawlerChannel: this.browserParams.crawlerChannel, - proxyId: this.browserParams.proxyId, - }; - - await this.saveProfile(params); - } - - private async saveProfile(params: { - name: string; - description?: string; - crawlerChannel?: string; - proxyId?: string | null; - }) { - this.isSubmitting = true; - - try { - let data: { id?: string; updated?: boolean; detail?: string } | undefined; - let retriesLeft = 300; - - while (retriesLeft > 0) { - if (this.browserParams.profileId) { - data = await this.api.fetch<{ - updated?: boolean; - detail?: string; - }>(`/orgs/${this.orgId}/profiles/${this.browserParams.profileId}`, { - method: "PATCH", - body: JSON.stringify({ - browserid: this.browserId, - name: params.name, - }), - }); - - if (data.updated !== undefined) { - break; - } - } else { - data = await this.api.fetch<{ id?: string; detail?: string }>( - `/orgs/${this.orgId}/profiles`, - { - method: "POST", - body: JSON.stringify({ - ...params, - browserid: this.browserId, - }), - }, - ); - } - - if (data.id) { - break; - } - if (data.detail === "waiting_for_browser") { - await new Promise((resolve) => setTimeout(resolve, 2000)); - } else { - throw new Error("unknown response"); - } - - retriesLeft -= 1; - } - - if (!retriesLeft) { - throw new Error("too many retries waiting for browser"); - } - - if (!data) { - throw new Error("unknown response"); - } - - this.notify.toast({ - message: msg("Successfully saved browser profile."), - variant: "success", - icon: "check2-circle", - id: "browser-profile-save-status", - }); - - this.navigate.to( - `${this.navigate.orgBasePath}/${OrgTab.BrowserProfiles}/profile/${this.browserParams.profileId || data.id}`, - ); - } catch (e) { - console.debug(e); - - this.isSubmitting = false; - - 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 update browser profiles.", - ); - } - } - - this.notify.toast({ - message: message, - variant: "danger", - icon: "exclamation-octagon", - id: "browser-profile-save-status", - }); - } - } - - private async createBrowser({ - url, - crawlerChannel, - proxyId, - profileId, - }: { - url: string; - crawlerChannel: string; - proxyId: string | null; - profileId?: string; - }) { - const params = { - url, - crawlerChannel, - proxyId, - profileId, - }; - - return this.api.fetch<{ browserid: string }>( - `/orgs/${this.orgId}/profiles/browser`, - { - method: "POST", - body: JSON.stringify(params), - }, - ); - } - - private async deleteBrowser(id: string) { - try { - const data = await this.api.fetch( - `/orgs/${this.orgId}/profiles/browser/${id}`, - { - method: "DELETE", - }, - ); - - return data; - } catch (e) { - // TODO Investigate DELETE returning 404 - console.debug(e); - } - } -} diff --git a/frontend/src/pages/org/index.ts b/frontend/src/pages/org/index.ts index 6b033d6030..2fe13688f8 100644 --- a/frontend/src/pages/org/index.ts +++ b/frontend/src/pages/org/index.ts @@ -61,7 +61,6 @@ import "./browser-profiles"; import(/* webpackChunkName: "org" */ "./archived-item-qa/archived-item-qa"); import(/* webpackChunkName: "org" */ "./workflows-new"); -import(/* webpackChunkName: "org" */ "./browser-profiles-new"); const RESOURCE_NAMES = ["workflow", "collection", "browser-profile", "upload"]; type ResourceName = (typeof RESOURCE_NAMES)[number]; From 5cdf4e72df97bece795905d4f11e0a7404055808 Mon Sep 17 00:00:00 2001 From: sua yoo Date: Fri, 7 Nov 2025 16:31:12 -0800 Subject: [PATCH 14/27] revert changes to card --- frontend/src/components/ui/card.ts | 12 +++---- frontend/src/layouts/secondaryPanel.ts | 45 -------------------------- 2 files changed, 6 insertions(+), 51 deletions(-) delete mode 100644 frontend/src/layouts/secondaryPanel.ts diff --git a/frontend/src/components/ui/card.ts b/frontend/src/components/ui/card.ts index 492832a37f..d94e6ee79b 100644 --- a/frontend/src/components/ui/card.ts +++ b/frontend/src/components/ui/card.ts @@ -2,18 +2,18 @@ import { html } from "lit"; import { customElement } from "lit/decorators.js"; import { TailwindElement } from "@/classes/TailwindElement"; -import { secondaryHeading } from "@/layouts/secondaryPanel"; @customElement("btrix-card") export class Card extends TailwindElement { render() { return html`
    - ${secondaryHeading({ - id: "cardHeading", - heading: html``, - })} - +
    + +
    diff --git a/frontend/src/layouts/secondaryPanel.ts b/frontend/src/layouts/secondaryPanel.ts deleted file mode 100644 index bc3663950b..0000000000 --- a/frontend/src/layouts/secondaryPanel.ts +++ /dev/null @@ -1,45 +0,0 @@ -import clsx from "clsx"; -import { html, nothing, type TemplateResult } from "lit"; -import { ifDefined } from "lit/directives/if-defined.js"; - -import { panelBody } from "./panel"; - -import { tw } from "@/utils/tailwind"; - -export function secondaryHeading({ - heading, - id, - srOnly, -}: { - heading: string | TemplateResult; - id?: string; - srOnly?: boolean; -}) { - return html`
    - ${heading} -
    `; -} - -export function secondaryPanel({ - heading, - body, - srOnly, -}: { - heading: string | TemplateResult; - body?: string | TemplateResult; - srOnly?: boolean; -}) { - return panelBody({ - content: html`
    - ${secondaryHeading({ id: "cardHeading", heading, srOnly })} - ${srOnly ? nothing : html``} -
    ${body}
    -
    `, - }); -} From 99cbd104dd7c4e04195ab4ad22dc5b2d7e1147e6 Mon Sep 17 00:00:00 2001 From: sua yoo Date: Fri, 7 Nov 2025 18:33:06 -0800 Subject: [PATCH 15/27] adjust for smaller screens --- .../browser-profiles/templates/badges.ts | 19 +++++++++---------- .../features/crawl-workflows/workflow-list.ts | 11 +++++++++++ frontend/src/layouts/pageHeader.ts | 2 +- frontend/src/layouts/panel.ts | 4 +--- .../src/pages/org/browser-profiles/browser.ts | 2 +- .../src/pages/org/browser-profiles/profile.ts | 18 +++++++++++------- 6 files changed, 34 insertions(+), 22 deletions(-) diff --git a/frontend/src/features/browser-profiles/templates/badges.ts b/frontend/src/features/browser-profiles/templates/badges.ts index 3c7c824928..303b2810dd 100644 --- a/frontend/src/features/browser-profiles/templates/badges.ts +++ b/frontend/src/features/browser-profiles/templates/badges.ts @@ -6,16 +6,15 @@ import capitalize from "lodash/fp/capitalize"; import { CrawlerChannelImage, type Profile } from "@/types/crawler"; export const usageBadge = (inUse: boolean) => - html` - - ${inUse ? msg("In Use") : msg("Not In Use")} - `; + html` + + + ${inUse ? msg("In Use") : msg("Not In Use")} + + `; export const badges = ( profile: Partial>, diff --git a/frontend/src/features/crawl-workflows/workflow-list.ts b/frontend/src/features/crawl-workflows/workflow-list.ts index 86d07113b5..85ae28e311 100644 --- a/frontend/src/features/crawl-workflows/workflow-list.ts +++ b/frontend/src/features/crawl-workflows/workflow-list.ts @@ -56,6 +56,13 @@ const rowCss = css` .row { display: grid; grid-template-columns: 1fr; + position: relative; + } + + .action { + position: absolute; + top: 0; + right: 0; } @media only screen and (min-width: ${mediumBreakpointCss}) { @@ -68,6 +75,10 @@ const rowCss = css` grid-template-columns: var(--btrix-workflow-list-columns); white-space: nowrap; } + + .action { + position: relative; + } } .col { diff --git a/frontend/src/layouts/pageHeader.ts b/frontend/src/layouts/pageHeader.ts index 2446b4a0e3..3b6f7478e2 100644 --- a/frontend/src/layouts/pageHeader.ts +++ b/frontend/src/layouts/pageHeader.ts @@ -128,7 +128,7 @@ export function pageHeader({ classNames, )} > -
    +
    ${prefix}${pageTitle(title)}${suffix}
    diff --git a/frontend/src/layouts/panel.ts b/frontend/src/layouts/panel.ts index f89c2022a0..e295647d1f 100644 --- a/frontend/src/layouts/panel.ts +++ b/frontend/src/layouts/panel.ts @@ -53,9 +53,7 @@ export function panel({ } & Parameters[0]) { return html`
    ${panelHeader({ heading, actions })} - + ${body}
    `; } diff --git a/frontend/src/pages/org/browser-profiles/browser.ts b/frontend/src/pages/org/browser-profiles/browser.ts index 35c16e319f..b3b526dda8 100644 --- a/frontend/src/pages/org/browser-profiles/browser.ts +++ b/frontend/src/pages/org/browser-profiles/browser.ts @@ -256,7 +256,7 @@ export class BrowserProfilesBrowserPage extends BtrixElement { } }} > - ${msg(this.profileId ? "Save Profile" : "Finish Browsing")} + ${this.profileId ? msg("Save Profile") : msg("Finish Browsing")}
    diff --git a/frontend/src/pages/org/browser-profiles/profile.ts b/frontend/src/pages/org/browser-profiles/profile.ts index aadab0a2fa..bf0c60eb4c 100644 --- a/frontend/src/pages/org/browser-profiles/profile.ts +++ b/frontend/src/pages/org/browser-profiles/profile.ts @@ -280,8 +280,8 @@ export class BrowserProfilesProfilePage extends BtrixElement { private readonly renderPage = () => { return html` -
    -
    +
    +
    ${this.renderProfile()} ${this.renderUsage()}
    @@ -300,7 +300,7 @@ export class BrowserProfilesProfilePage extends BtrixElement { this.browserIdTask.value; return panel({ - heading: msg("Visited Sites"), + heading: msg("Configured Sites"), actions: this.appState.isCrawler ? html` profile.origins.map( (origin) => html` -
  • +
  • - +
    `; return panel({ - heading: msg("Usage"), + heading: msg("Related Workflows"), body: when( this.profile, (profile) => @@ -561,7 +565,7 @@ export class BrowserProfilesProfilePage extends BtrixElement { const failedNotLoggedInState = "failed_not_logged_in" satisfies CrawlState; return html` -
    +
    ${msg("Filter by:")} From fb18fa40230f9db559c63c7ef1153572289cd326 Mon Sep 17 00:00:00 2001 From: sua yoo Date: Fri, 7 Nov 2025 18:35:06 -0800 Subject: [PATCH 16/27] adjust for smaller screens --- frontend/src/layouts/page.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/layouts/page.ts b/frontend/src/layouts/page.ts index 0e5214a80b..a84710480c 100644 --- a/frontend/src/layouts/page.ts +++ b/frontend/src/layouts/page.ts @@ -39,7 +39,7 @@ export function page( >
    ${header.breadcrumbs ? html` ${pageNav(header.breadcrumbs)} ` : nothing} ${header.title ? pageHeader(header) : nothing} From 08dd549ddfa0fb593866ca3438f78b73beaefddf Mon Sep 17 00:00:00 2001 From: sua yoo Date: Sat, 8 Nov 2025 15:28:48 -0800 Subject: [PATCH 17/27] show modified by crawl --- .../profile-metadata-dialog.ts | 17 +++-- .../src/pages/org/browser-profiles/profile.ts | 64 ++++++++++++++----- 2 files changed, 57 insertions(+), 24 deletions(-) diff --git a/frontend/src/features/browser-profiles/profile-metadata-dialog.ts b/frontend/src/features/browser-profiles/profile-metadata-dialog.ts index dd68e0f0ee..f73310b096 100644 --- a/frontend/src/features/browser-profiles/profile-metadata-dialog.ts +++ b/frontend/src/features/browser-profiles/profile-metadata-dialog.ts @@ -174,13 +174,16 @@ export class ProfileMetadataDialog extends BtrixElement { @sl-input=${this.validateDescriptionMax.validate} > - + ${ + // + undefined + }
    - - ${this.renderDetail(() => html`${none}`)} - + ${ + // + // ${this.renderDetail(() => html`${none}`)} + // + undefined + } @@ -459,16 +462,29 @@ export class BrowserProfilesProfilePage extends BtrixElement { ), )} - - ${this.renderDetail((profile) => { - const userName = profile.modifiedByName || profile.createdByName; - if (userName) { - return `${msg("Updated by")} ${userName}`; - } - - return stringFor.notApplicable; - })} - + ${when(this.profile, (profile) => + profile.modified + ? html` + ${this.renderDetail((profile) => { + if ( + profile.modifiedCrawlDate && + (!profile.modified || + profile.modifiedCrawlDate >= profile.modified) + ) { + return msg("Automatic update from crawl"); + } + + if (profile.modifiedByName) { + return profile.modifiedByName; + } + + return noData; + })} + ` + : html` + ${profile.createdByName || noData} + `, + )} ${this.renderDetail((profile) => { const isBackedUp = @@ -505,8 +521,22 @@ export class BrowserProfilesProfilePage extends BtrixElement { ? `${this.localize.number(this.workflowsTask.value.total)} ${pluralOf("workflows", this.workflowsTask.value.total)}` : html``} - - ${msg("No")} + + ${profile.modifiedCrawlId && profile.modifiedCrawlDate + ? html` + ${this.localize.relativeDate( + profile.modifiedCrawlDate, + )} + + ${msg("View Crawl")} + ` + : msg("No")}
    From 8f2f6f9434970546cd65fa74d322555d183bfbd2 Mon Sep 17 00:00:00 2001 From: sua yoo Date: Mon, 10 Nov 2025 10:15:12 -0800 Subject: [PATCH 18/27] update buttons --- .../src/pages/org/browser-profiles/profile.ts | 136 ++++++++++-------- 1 file changed, 74 insertions(+), 62 deletions(-) diff --git a/frontend/src/pages/org/browser-profiles/profile.ts b/frontend/src/pages/org/browser-profiles/profile.ts index c7a9caa78c..f65ea3bb57 100644 --- a/frontend/src/pages/org/browser-profiles/profile.ts +++ b/frontend/src/pages/org/browser-profiles/profile.ts @@ -49,6 +49,7 @@ import { isNotEqual } from "@/utils/is-not-equal"; import { isArchivingDisabled } from "@/utils/orgs"; import { pluralOf } from "@/utils/pluralize"; import { tw } from "@/utils/tailwind"; +import type { SectionsEnum } from "@/utils/workflow"; const INITIAL_PAGE_SIZE = 5; const WORKFLOW_PAGE_QUERY = "workflowsPage"; @@ -302,13 +303,13 @@ export class BrowserProfilesProfilePage extends BtrixElement { return panel({ heading: msg("Configured Sites"), actions: this.appState.isCrawler - ? html` - (this.openDialog = "config")} - > - ` + ? html` (this.openDialog = "config")} + > + + ${msg("Configure Profile")} + ` : undefined, body: html`${this.renderOrigins()} @@ -351,36 +352,36 @@ export class BrowserProfilesProfilePage extends BtrixElement { const origins = (profile: Profile) => profile.origins.map( (origin) => html` -
  • -
    -
    - - -
    - -
    +
  • + + +
    - - { - this.initialNavigateUrl = origin; - this.openBrowser(); - }} - > - + + { - this.navigate.to( - `${this.navigate.orgBasePath}/${OrgTab.Workflows}/new`, - settingsForDuplicate({ - workflow: { - profileid: this.profileId, - proxyId: profile.proxyId, - crawlerChannel: profile.crawlerChannel, - }, - }), - ); - - this.notify.toast({ - message: msg( - "Copied browser settings to new workflow.", - ), - variant: "success", - icon: "check2-circle", - id: "workflow-copied-status", - }); - }} - > - - ${msg("Create Workflow Using Profile")}`, + actions: html`
    + + + ${msg("Manage Workflows")} + + { + this.navigate.to( + `${this.navigate.orgBasePath}/${OrgTab.Workflows}/new#${"browserSettings" satisfies SectionsEnum}`, + settingsForDuplicate({ + workflow: { + profileid: this.profileId, + proxyId: profile.proxyId, + crawlerChannel: profile.crawlerChannel, + }, + }), + ); + + this.notify.toast({ + message: msg( + "Copied browser settings to new workflow.", + ), + variant: "success", + icon: "check2-circle", + id: "workflow-copied-status", + duration: 8000, + }); + }} + > + + ${msg("New Workflow with Profile")} +
    `, }), }), workflowListSkeleton, From c889bebc4ddfbfe8c1bfea5f24e2a19913e8ba61 Mon Sep 17 00:00:00 2001 From: sua yoo Date: Mon, 10 Nov 2025 21:10:42 -0800 Subject: [PATCH 19/27] refactor to use single dialog --- frontend/src/components/ui/dialog.ts | 9 +- frontend/src/components/ui/url-input.ts | 6 +- .../src/features/browser-profiles/index.ts | 3 +- .../new-browser-profile-dialog.ts | 224 ++++++++ .../profile-browser-dialog.ts | 405 ++++++++++++++ .../browser-profiles/profile-browser.ts | 67 ++- .../profile-metadata-dialog.ts | 6 +- .../profile-settings-dialog.ts | 257 --------- .../src/features/browser-profiles/types.ts | 12 + .../src/pages/org/browser-profiles-list.ts | 57 +- .../src/pages/org/browser-profiles/browser.ts | 516 ------------------ .../src/pages/org/browser-profiles/index.ts | 1 - .../src/pages/org/browser-profiles/profile.ts | 386 ++++++------- frontend/src/pages/org/index.ts | 17 +- 14 files changed, 903 insertions(+), 1063 deletions(-) create mode 100644 frontend/src/features/browser-profiles/new-browser-profile-dialog.ts create mode 100644 frontend/src/features/browser-profiles/profile-browser-dialog.ts delete mode 100644 frontend/src/features/browser-profiles/profile-settings-dialog.ts create mode 100644 frontend/src/features/browser-profiles/types.ts delete mode 100644 frontend/src/pages/org/browser-profiles/browser.ts diff --git a/frontend/src/components/ui/dialog.ts b/frontend/src/components/ui/dialog.ts index 5bd89544b4..d17cfffa29 100644 --- a/frontend/src/components/ui/dialog.ts +++ b/frontend/src/components/ui/dialog.ts @@ -10,6 +10,10 @@ import { * with custom CSS * * Usage: see https://shoelace.style/components/dialog + * + * @attr label + * @attr open + * @attr noHeader */ @customElement("btrix-dialog") export class Dialog extends SlDialog { @@ -27,8 +31,7 @@ export class Dialog extends SlDialog { } .dialog__header { - background-color: var(--sl-color-neutral-50); - border-bottom: 1px solid var(--sl-color-neutral-100); + border-bottom: 1px solid var(--sl-panel-border-color); } .dialog__title { @@ -50,7 +53,7 @@ export class Dialog extends SlDialog { .dialog__footer { padding-top: var(--sl-spacing-small); padding-bottom: var(--sl-spacing-small); - border-top: 1px solid var(--sl-color-neutral-100); + border-top: 1px solid var(--sl-panel-border-color); } `, ]; diff --git a/frontend/src/components/ui/url-input.ts b/frontend/src/components/ui/url-input.ts index 4fc3108b82..3a1dd916ac 100644 --- a/frontend/src/components/ui/url-input.ts +++ b/frontend/src/components/ui/url-input.ts @@ -42,6 +42,11 @@ export class UrlInput extends SlInput { this.addEventListener("sl-change", this.onChange); } + setCustomValidity(message: string): void { + super.setCustomValidity(message); + if (!this.hideHelpText) this.helpText = message; + } + disconnectedCallback(): void { super.disconnectedCallback(); @@ -61,7 +66,6 @@ export class UrlInput extends SlInput { if (value && !validURL(value)) { const text = msg("Please enter a valid URL."); - if (!this.hideHelpText) this.helpText = text; this.setCustomValidity(text); } else if ( value && diff --git a/frontend/src/features/browser-profiles/index.ts b/frontend/src/features/browser-profiles/index.ts index c4ba0e0904..c84fc0e980 100644 --- a/frontend/src/features/browser-profiles/index.ts +++ b/frontend/src/features/browser-profiles/index.ts @@ -1,4 +1,5 @@ +import("./new-browser-profile-dialog"); +import("./profile-browser-dialog"); import("./profile-browser"); import("./profile-metadata-dialog"); -import("./profile-settings-dialog"); import("./select-browser-profile"); diff --git a/frontend/src/features/browser-profiles/new-browser-profile-dialog.ts b/frontend/src/features/browser-profiles/new-browser-profile-dialog.ts new file mode 100644 index 0000000000..d6769933be --- /dev/null +++ b/frontend/src/features/browser-profiles/new-browser-profile-dialog.ts @@ -0,0 +1,224 @@ +import { localized, msg } from "@lit/localize"; +import { type SlInput } from "@shoelace-style/shoelace"; +import { html, nothing, type PropertyValues } from "lit"; +import { + customElement, + property, + query, + queryAsync, + state, +} from "lit/decorators.js"; +import { ifDefined } from "lit/directives/if-defined.js"; +import { when } from "lit/directives/when.js"; + +import { BtrixElement } from "@/classes/BtrixElement"; +import type { Dialog } from "@/components/ui/dialog"; +import { type SelectCrawlerChangeEvent } from "@/components/ui/select-crawler"; +import { type SelectCrawlerProxyChangeEvent } from "@/components/ui/select-crawler-proxy"; +import { + CrawlerChannelImage, + type CrawlerChannel, + type Proxy, +} from "@/types/crawler"; + +@customElement("btrix-new-browser-profile-dialog") +@localized() +export class NewBrowserProfileDialog extends BtrixElement { + @property({ type: String }) + defaultUrl?: string; + + @property({ type: String }) + defaultProxyId?: string; + + @property({ type: String }) + defaultCrawlerChannel?: string; + + @property({ type: Array }) + proxyServers?: Proxy[]; + + @property({ type: Array }) + crawlerChannels?: CrawlerChannel[]; + + @property({ type: Boolean }) + open = false; + + @state() + browserOpen = false; + + @state() + private name?: string; + + @state() + private url?: string; + + @state() + private crawlerChannel: CrawlerChannel["id"] = CrawlerChannelImage.Default; + + @state() + private proxyId: string | null = null; + + @query("btrix-dialog") + private readonly dialog?: Dialog; + + @queryAsync("#browserProfileForm") + private readonly form!: Promise; + + connectedCallback(): void { + super.connectedCallback(); + + if (this.org?.crawlingDefaults) { + if (!this.defaultProxyId) + this.defaultProxyId = this.org.crawlingDefaults.proxyId; + if (!this.defaultCrawlerChannel) + this.defaultCrawlerChannel = this.org.crawlingDefaults.crawlerChannel; + } + } + + protected willUpdate(changedProperties: PropertyValues): void { + if (changedProperties.has("defaultProxyId") && this.defaultProxyId) { + this.proxyId = this.proxyId || this.defaultProxyId; + } + + if ( + changedProperties.has("defaultCrawlerChannel") && + this.defaultCrawlerChannel + ) { + this.crawlerChannel = + (this.crawlerChannel !== CrawlerChannelImage.Default && + this.crawlerChannel) || + this.defaultCrawlerChannel; + } + } + + render() { + return html` + { + const nameInput = (await this.form).querySelector( + "btrix-url-input", + ); + if (nameInput) { + e.preventDefault(); + nameInput.focus(); + } + }} + > +
    + + + + ${this.crawlerChannels && this.crawlerChannels.length > 1 + ? html`
    + + (this.crawlerChannel = e.detail.value!)} + > +
    ` + : nothing} + ${this.proxyServers?.length + ? html` +
    + + (this.proxyId = e.detail.value)} + > +
    + ` + : nothing} + + + + + +
    +
    + { + // Using reset method instead of type="reset" fixes + // incorrect getRootNode in Chrome + (await this.form).reset(); + }} + >${msg("Cancel")} + this.dialog?.submit()} + > + ${msg("Start Browser")} + +
    +
    + + ${when( + this.url, + (url) => + html` {}} + @sl-after-hide=${() => {}} + > + `, + )} + `; + } + + private async hideDialog() { + void (await this.form).closest("btrix-dialog")?.hide(); + } + + private onReset() { + void this.hideDialog(); + } + + private async onSubmit(event: SubmitEvent) { + event.preventDefault(); + + const form = event.target as HTMLFormElement; + + if (!form.checkValidity()) { + return; + } + + const formData = new FormData(form); + this.name = formData.get("profile-name") as string; + this.url = formData.get("profile-url") as string; + + await this.updateComplete; + + this.browserOpen = true; + } +} diff --git a/frontend/src/features/browser-profiles/profile-browser-dialog.ts b/frontend/src/features/browser-profiles/profile-browser-dialog.ts new file mode 100644 index 0000000000..e70fd15398 --- /dev/null +++ b/frontend/src/features/browser-profiles/profile-browser-dialog.ts @@ -0,0 +1,405 @@ +import { localized, msg } from "@lit/localize"; +import { Task, TaskStatus } from "@lit/task"; +import clsx from "clsx"; +import { html, nothing } from "lit"; +import { customElement, property, query, state } from "lit/decorators.js"; +import { ifDefined } from "lit/directives/if-defined.js"; +import { when } from "lit/directives/when.js"; + +import { badges } from "./templates/badges"; + +import { BtrixElement } from "@/classes/BtrixElement"; +import type { Dialog } from "@/components/ui/dialog"; +import type { + BrowserConnectionChange, + ProfileBrowser, +} from "@/features/browser-profiles/profile-browser"; +import type { + CreateBrowserOptions, + ProfileUpdatedEvent, +} from "@/features/browser-profiles/types"; +import { OrgTab } from "@/routes"; +import type { Profile } from "@/types/crawler"; +import { isApiError } from "@/utils/api"; +import { tw } from "@/utils/tailwind"; + +/** + * @fires btrix-updated + */ +@customElement("btrix-profile-browser-dialog") +@localized() +export class ProfileBrowserDialog extends BtrixElement { + @property({ type: Object }) + profile?: Profile; + + @property({ type: Object }) + config?: CreateBrowserOptions & { name?: string }; + + @property({ type: Boolean }) + duplicating = false; + + @property({ type: Boolean }) + open = false; + + @state() + private isBrowserLoaded = false; + + @query("btrix-dialog") + private readonly dialog?: Dialog | null; + + @query("btrix-profile-browser") + private readonly profileBrowser?: ProfileBrowser | null; + + #savedBrowserId?: string; + + private readonly browserIdTask = new Task(this, { + task: async ([open, config, profile], { signal }) => { + if (!open || !config) return null; + + if (this.browserIdTask.value) { + // Delete previously created and unused browser + void this.deleteBrowser(this.browserIdTask.value, signal); + } + + const { browserid } = await this.createBrowser( + { profileId: profile?.id, ...config }, + signal, + ); + + return browserid; + }, + args: () => [this.open, this.config, this.profile] as const, + }); + + disconnectedCallback(): void { + const browserId = this.browserIdTask.value; + + if (browserId && browserId !== this.#savedBrowserId) { + void this.deleteBrowser(browserId); + } + + super.disconnectedCallback(); + } + + private readonly saveProfileTask = new Task(this, { + autoRun: false, + task: async ([browserId, config], { signal }) => { + if (!browserId || !config) return; + + try { + const data = await this.saveProfile( + { + browserId, + name: + config.name || + this.profile?.name || + new URL(config.url).origin.slice(0, 50), + }, + signal, + ); + + this.#savedBrowserId = browserId; + + this.dispatchEvent( + new CustomEvent("btrix-updated"), + ); + + return data; + } catch (err) { + let message = msg("Sorry, couldn't save browser profile at this time."); + + if (isApiError(err) && err.statusCode === 403) { + if (err.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 update browser profiles.", + ); + } + } + + throw message; + } + }, + args: () => [this.browserIdTask.value, this.config] as const, + }); + + render() { + const isCrawler = this.appState.isCrawler; + const creatingNew = this.duplicating || !this.profile; + const saving = this.saveProfileTask.status === TaskStatus.PENDING; + + return html` { + // Hide viewport scrollbar when full screen dialog is open + document.documentElement.classList.add(tw`overflow-hidden`); + }} + @sl-hide=${() => { + document.documentElement.classList.remove(tw`overflow-hidden`); + }} + @sl-after-hide=${() => this.closeBrowser()} + > +
    +
    +
    + { + this.saveProfileTask.abort(); + + if (this.browserIdTask.value) { + void this.deleteBrowser(this.browserIdTask.value); + } + + void this.dialog?.hide(); + }} + > + +
    +
    +

    + ${[this.config?.name || this.profile?.name, this.config?.url] + .filter((v) => v) + .join(" — ")} +

    + ${when( + creatingNew ? this.config : this.profile || this.config, + badges, + )} +
    +
    +
    +
    + + void this.profileBrowser?.enterFullscreen()} + > + + + this.profileBrowser?.toggleOrigins()} + > + +
    + + ${when( + isCrawler, + () => html` + +
    + void this.submit()} + > + ${creatingNew ? msg("Create Profile") : msg("Save Profile")} + +
    +
    + `, + )} +
    +
    + +
    + ${this.browserIdTask.render({ + complete: (browserId) => + browserId + ? html`` + : nothing, + })} +
    +
    `; + } + + private readonly closeBrowser = () => { + this.isBrowserLoaded = false; + }; + + private readonly onBrowserLoad = () => { + this.isBrowserLoaded = true; + }; + + private readonly onBrowserReload = () => { + this.isBrowserLoaded = false; + }; + + private readonly onBrowserError = () => { + this.isBrowserLoaded = false; + }; + + private readonly onBrowserConnectionChange = ( + e: CustomEvent, + ) => { + this.isBrowserLoaded = e.detail.connected; + }; + + private async submit() { + this.notify.toast({ + message: msg("Saving profile..."), + variant: "primary", + icon: "info-circle", + id: "browser-profile-save-status", + duration: Infinity, + }); + + try { + const dialog = this.dialog; + + await this.saveProfileTask.run(); + + if (this.saveProfileTask.value?.id) { + void dialog?.hide(); + this.navigate.to( + `${this.navigate.orgBasePath}/${OrgTab.BrowserProfiles}/profile/${this.saveProfileTask.value.id}`, + ); + } else { + await dialog?.hide(); + } + + this.notify.toast({ + message: msg("Successfully saved browser profile."), + variant: "success", + icon: "check2-circle", + id: "browser-profile-save-status", + }); + } catch (err) { + if (typeof err === "string") { + this.notify.toast({ + message: err, + variant: "danger", + icon: "exclamation-octagon", + id: "browser-profile-save-status", + }); + } else { + console.debug(err); + } + } + } + + private async saveProfile( + params: { browserId: string; name: string }, + signal: AbortSignal, + ) { + const profileId = !this.duplicating && this.profile?.id; + const payload = { + browserid: params.browserId, + name: params.name, + }; + + let data: { id?: string; updated?: boolean; detail?: string } | undefined; + let retriesLeft = 300; + + while (retriesLeft > 0) { + if (profileId) { + data = await this.api.fetch<{ + updated?: boolean; + detail?: string; + }>(`/orgs/${this.orgId}/profiles/${profileId}`, { + method: "PATCH", + body: JSON.stringify(payload), + signal, + }); + + if (data.updated !== undefined) { + break; + } + } else { + data = await this.api.fetch<{ id?: string; detail?: string }>( + `/orgs/${this.orgId}/profiles`, + { + method: "POST", + body: JSON.stringify(payload), + signal, + }, + ); + } + + if (data.id) { + break; + } + if (data.detail === "waiting_for_browser") { + await new Promise((resolve) => setTimeout(resolve, 2000)); + } else { + throw new Error("unknown response"); + } + + retriesLeft -= 1; + } + + if (!retriesLeft) { + throw new Error("too many retries waiting for browser"); + } + + if (!data) { + throw new Error("unknown response"); + } + + return data; + } + + private async createBrowser( + params: CreateBrowserOptions, + signal?: AbortSignal, + ) { + return this.api.fetch<{ browserid: string }>( + `/orgs/${this.orgId}/profiles/browser`, + { + method: "POST", + body: JSON.stringify(params), + signal, + }, + ); + } + + private async deleteBrowser(browserId: string, signal?: AbortSignal) { + try { + const data = await this.api.fetch( + `/orgs/${this.orgId}/profiles/browser/${browserId}`, + { + method: "DELETE", + signal, + }, + ); + + return data; + } catch (err) { + if (isApiError(err) && err.statusCode === 404) { + // Safe to ignore, since unloaded browser will have already been deleted + } else { + console.debug(err); + } + } + } +} diff --git a/frontend/src/features/browser-profiles/profile-browser.ts b/frontend/src/features/browser-profiles/profile-browser.ts index cab11686a5..cd416ba8e8 100644 --- a/frontend/src/features/browser-profiles/profile-browser.ts +++ b/frontend/src/features/browser-profiles/profile-browser.ts @@ -1,6 +1,5 @@ import { localized, msg, str } from "@lit/localize"; -import { Task } from "@lit/task"; -import clsx from "clsx"; +import { Task, TaskStatus } from "@lit/task"; import { html, type PropertyValues } from "lit"; import { customElement, property, query, state } from "lit/decorators.js"; import { cache } from "lit/directives/cache.js"; @@ -9,7 +8,6 @@ import { when } from "lit/directives/when.js"; import { BtrixElement } from "@/classes/BtrixElement"; import { emptyMessage } from "@/layouts/emptyMessage"; import { isApiError, type APIError } from "@/utils/api"; -import { tw } from "@/utils/tailwind"; const POLL_INTERVAL_SECONDS = 2; const hiddenClassList = ["translate-x-2/3", "opacity-0", "pointer-events-none"]; @@ -48,6 +46,7 @@ const isPolling = (value: unknown): value is number => { * @fires btrix-browser-connection-change * @cssPart base * @cssPart browser + * @cssPart iframe */ @customElement("btrix-profile-browser") @localized() @@ -58,9 +57,6 @@ export class ProfileBrowser extends BtrixElement { @property({ type: String }) initialNavigateUrl?: string; - @property({ type: Boolean }) - readOnly = false; - @property({ type: Boolean }) hideControls = false; @@ -130,7 +126,7 @@ export class ProfileBrowser extends BtrixElement { } if (this.initialNavigateUrl) { - await this.navigateBrowser({ url: this.initialNavigateUrl }); + await this.navigateBrowser({ url: this.initialNavigateUrl }, signal); } window.addEventListener("beforeunload", this.onBeforeUnload); @@ -214,9 +210,8 @@ export class ProfileBrowser extends BtrixElement { } private readonly onBeforeUnload = (e: BeforeUnloadEvent) => { - if (!this.readOnly) { - e.preventDefault(); - } + console.log("e", e); + // e.preventDefault(); }; private readonly onBrowserError = async () => { @@ -268,31 +263,32 @@ export class ProfileBrowser extends BtrixElement { const browserLoading = () => cache( html`
    -

    +

    ${msg("Loading interactive browser...")}

    `, ); return html` -
    - ${when(!this.hideControls, this.renderControlBar)} +
    + ${this.renderControlBar()}
    @@ -300,7 +296,7 @@ export class ProfileBrowser extends BtrixElement { initial: browserLoading, pending: browserLoading, error: () => html` -
    +

    ${msg( @@ -342,7 +338,16 @@ export class ProfileBrowser extends BtrixElement { ), )} `, - () => emptyMessage({ message: msg("No visited sites yet.") }), + () => + this.browserTask.status === TaskStatus.PENDING + ? emptyMessage({ + message: msg( + "Sites will be shown here once the browser is done loading.", + ), + }) + : emptyMessage({ + message: msg("No sites configured yet."), + }), )}

    @@ -368,6 +373,8 @@ export class ProfileBrowser extends BtrixElement { `; } + if (this.hideControls) return; + return html`
    @@ -404,13 +411,11 @@ export class ProfileBrowser extends BtrixElement { browser.url, (url) => html` `, )} @@ -435,7 +440,7 @@ export class ProfileBrowser extends BtrixElement { private renderSidebarButton() { return html` - +
    -

    ${msg("Visited Sites")}

    +

    ${msg("Saved Sites")}

    ; - /** * @fires btrix-updated */ diff --git a/frontend/src/features/browser-profiles/profile-settings-dialog.ts b/frontend/src/features/browser-profiles/profile-settings-dialog.ts deleted file mode 100644 index 9d9124e4de..0000000000 --- a/frontend/src/features/browser-profiles/profile-settings-dialog.ts +++ /dev/null @@ -1,257 +0,0 @@ -import { localized, msg } from "@lit/localize"; -import { type SlInput } from "@shoelace-style/shoelace"; -import { html, nothing, type PropertyValues } from "lit"; -import { - customElement, - property, - query, - queryAsync, - state, -} from "lit/decorators.js"; -import { ifDefined } from "lit/directives/if-defined.js"; -import queryString from "query-string"; - -import { BtrixElement } from "@/classes/BtrixElement"; -import type { Dialog } from "@/components/ui/dialog"; -import { type SelectCrawlerChangeEvent } from "@/components/ui/select-crawler"; -import { type SelectCrawlerProxyChangeEvent } from "@/components/ui/select-crawler-proxy"; -import { - CrawlerChannelImage, - type CrawlerChannel, - type Profile, - type Proxy, -} from "@/types/crawler"; - -@customElement("btrix-profile-settings-dialog") -@localized() -export class ProfileSettingsDialog extends BtrixElement { - @property({ type: Object }) - profile?: Profile; - - @property({ type: String }) - defaultUrl?: string; - - @property({ type: String }) - defaultProxyId?: string; - - @property({ type: String }) - defaultCrawlerChannel?: string; - - @property({ type: Array }) - proxyServers?: Proxy[]; - - @property({ type: Array }) - crawlerChannels?: CrawlerChannel[]; - - @property({ type: Boolean }) - open = false; - - @state() - private isSubmitting = false; - - @state() - private crawlerChannel: CrawlerChannel["id"] = CrawlerChannelImage.Default; - - @state() - private proxyId: string | null = null; - - @query("btrix-dialog") - private readonly dialog?: Dialog; - - @queryAsync("#browserProfileForm") - private readonly form!: Promise; - - connectedCallback(): void { - super.connectedCallback(); - - if (this.org?.crawlingDefaults) { - if (!this.defaultProxyId) - this.defaultProxyId = this.org.crawlingDefaults.proxyId; - if (!this.defaultCrawlerChannel) - this.defaultCrawlerChannel = this.org.crawlingDefaults.crawlerChannel; - } - } - - protected willUpdate(changedProperties: PropertyValues): void { - if (changedProperties.has("defaultProxyId") && this.defaultProxyId) { - this.proxyId = this.proxyId || this.defaultProxyId; - } - - if ( - changedProperties.has("defaultCrawlerChannel") && - this.defaultCrawlerChannel - ) { - this.crawlerChannel = - (this.crawlerChannel !== CrawlerChannelImage.Default && - this.crawlerChannel) || - this.defaultCrawlerChannel; - } - } - - render() { - return html` { - const nameInput = (await this.form).querySelector( - "btrix-url-input", - ); - if (nameInput) { - e.preventDefault(); - nameInput.focus(); - } - }} - > -
    - - - - ${this.crawlerChannels && this.crawlerChannels.length > 1 - ? html`
    - - (this.crawlerChannel = e.detail.value!)} - > -
    ` - : nothing} - ${this.proxyServers?.length - ? html` -
    - - (this.proxyId = e.detail.value)} - > -
    - ` - : nothing} - - -
    -
    - { - // Using reset method instead of type="reset" fixes - // incorrect getRootNode in Chrome - (await this.form).reset(); - }} - >${msg("Cancel")} - this.dialog?.submit()} - > - ${msg("Start Browser")} - -
    -
    `; - } - - private async hideDialog() { - void (await this.form).closest("btrix-dialog")?.hide(); - } - - private onReset() { - void this.hideDialog(); - } - - private async onSubmit(event: SubmitEvent) { - event.preventDefault(); - - const form = event.target as HTMLFormElement; - - if (!form.checkValidity()) { - return; - } - - this.isSubmitting = true; - - const formData = new FormData(form); - let url = formData.get("url") as string; - - try { - url = url.trim(); - if (!url.startsWith("http://") && !url.startsWith("https://")) { - url = `https://${url}`; - } - const data = await this.createBrowser({ - url: url, - crawlerChannel: this.crawlerChannel, - proxyId: this.proxyId, - profileId: this.profile?.id, - }); - - this.notify.toast({ - message: msg("Starting up browser..."), - variant: "success", - icon: "check2-circle", - id: "browser-profile-update-status", - }); - await this.hideDialog(); - this.navigate.to( - `${this.navigate.orgBasePath}/browser-profiles/profile${this.profile ? `/${this.profile.id}` : ""}/browser/${ - data.browserid - }?${queryString.stringify({ - url, - crawlerChannel: this.crawlerChannel, - proxyId: this.proxyId, - })}`, - ); - } catch (e) { - this.notify.toast({ - message: msg("Sorry, couldn't create browser profile at this time."), - variant: "danger", - icon: "exclamation-octagon", - id: "browser-profile-update-status", - }); - } - this.isSubmitting = false; - } - - private async createBrowser({ - url, - crawlerChannel, - proxyId, - profileId, - }: { - url: string; - crawlerChannel: string; - proxyId: string | null; - profileId?: string; - }) { - const params = { - url, - crawlerChannel, - proxyId, - profileId, - }; - - return this.api.fetch<{ browserid: string }>( - `/orgs/${this.orgId}/profiles/browser`, - { - method: "POST", - body: JSON.stringify(params), - }, - ); - } -} diff --git a/frontend/src/features/browser-profiles/types.ts b/frontend/src/features/browser-profiles/types.ts new file mode 100644 index 0000000000..57c15dd399 --- /dev/null +++ b/frontend/src/features/browser-profiles/types.ts @@ -0,0 +1,12 @@ +import type { Profile } from "@/types/crawler"; + +export type ProfileUpdatedEvent = CustomEvent< + Partial> +>; + +export type CreateBrowserOptions = { + url: string; + profileId?: string; + crawlerChannel?: string; + proxyId?: string; +}; diff --git a/frontend/src/pages/org/browser-profiles-list.ts b/frontend/src/pages/org/browser-profiles-list.ts index 199f9de10f..f89165a3df 100644 --- a/frontend/src/pages/org/browser-profiles-list.ts +++ b/frontend/src/pages/org/browser-profiles-list.ts @@ -74,6 +74,12 @@ const columnsCss = [ @customElement("btrix-browser-profiles-list") @localized() export class BrowserProfilesList extends BtrixElement { + @state() + private selectedProfile?: Profile; + + @state() + private openDialog?: "duplicate"; + @state() private pagination: Required = { page: parsePage(new URLSearchParams(location.search).get("page")), @@ -227,9 +233,29 @@ export class BrowserProfilesList extends BtrixElement { : this.renderEmpty()} `, )} + ${when(this.selectedProfile, this.renderDuplicateDialog)} `; }; + private readonly renderDuplicateDialog = (profile: Profile) => { + return html` { + this.selectedProfile = undefined; + this.openDialog = undefined; + }} + > + `; + }; + private renderEmpty() { const message = msg("Your org doesn’t have any browser profiles yet."); @@ -464,34 +490,9 @@ export class BrowserProfilesList extends BtrixElement { } private async duplicateProfile(profile: Profile) { - const url = profile.origins[0]; - - try { - const data = await this.createBrowser({ url }); - - this.notify.toast({ - message: msg("Starting up browser..."), - variant: "success", - icon: "check2-circle", - }); - - this.navigate.to( - `${this.navigate.orgBasePath}/browser-profiles/profile/browser/${ - data.browserid - }?${queryString.stringify({ - url, - name: `${profile.name} ${msg("Copy")}`, - crawlerChannel: profile.crawlerChannel, - proxyId: profile.proxyId, - })}`, - ); - } catch (e) { - this.notify.toast({ - message: msg("Sorry, couldn't create browser profile at this time."), - variant: "danger", - icon: "exclamation-octagon", - }); - } + this.selectedProfile = profile; + await this.updateComplete; + this.openDialog = "duplicate"; } private async deleteProfile(profile: Profile) { diff --git a/frontend/src/pages/org/browser-profiles/browser.ts b/frontend/src/pages/org/browser-profiles/browser.ts deleted file mode 100644 index b3b526dda8..0000000000 --- a/frontend/src/pages/org/browser-profiles/browser.ts +++ /dev/null @@ -1,516 +0,0 @@ -import { localized, msg, str } from "@lit/localize"; -import { Task } from "@lit/task"; -import { html } from "lit"; -import { customElement, property, query, state } from "lit/decorators.js"; -import { ifDefined } from "lit/directives/if-defined.js"; -import { when } from "lit/directives/when.js"; -import queryString from "query-string"; - -import { BtrixElement } from "@/classes/BtrixElement"; -import type { Dialog } from "@/components/ui/dialog"; -import type { BtrixUserGuideShowEvent } from "@/events/btrix-user-guide-show"; -import type { BrowserConnectionChange } from "@/features/browser-profiles/profile-browser"; -import { - badges, - badgesSkeleton, -} from "@/features/browser-profiles/templates/badges"; -import { page } from "@/layouts/page"; -import { type Breadcrumb } from "@/layouts/pageHeader"; -import { OrgTab } from "@/routes"; -import { CrawlerChannelImage, type Profile } from "@/types/crawler"; -import { isApiError } from "@/utils/api"; -import { isNotEqual } from "@/utils/is-not-equal"; - -@customElement("btrix-browser-profiles-browser-page") -@localized() -export class BrowserProfilesBrowserPage extends BtrixElement { - @property({ type: String }) - profileId?: string; - - @property({ type: String }) - browserId?: string; - - @property({ type: Object, attribute: false, hasChanged: isNotEqual }) - config: { - url: string; - name?: string; - crawlerChannel?: string; - proxyId?: string; - } = { - url: "", - }; - - @state() - private isSubmitting = false; - - @state() - private isDialogVisible = false; - - @state() - private isBrowserLoaded = false; - - @query("#discardDialog") - private readonly discardDialog?: Dialog | null; - - private readonly profileTask = new Task(this, { - task: async ([profileId], { signal }) => { - if (!profileId) return; - - const profile = await this.getProfile(profileId, signal); - - return profile; - }, - args: () => [this.profileId] as const, - }); - - disconnectedCallback(): void { - super.disconnectedCallback(); - - if (this.browserId) { - void this.deleteBrowser(this.browserId); - } - } - - render() { - if (!this.browserId) { - return html`
    - -
    `; - } - - const profile = this.profileTask.value; - - let breadcrumbs: Breadcrumb[] = [ - { - href: `${this.navigate.orgBasePath}/${OrgTab.BrowserProfiles}`, - content: msg("Browser Profiles"), - }, - ]; - - if (this.profileId) { - breadcrumbs = [ - ...breadcrumbs, - { - href: `${this.navigate.orgBasePath}/${OrgTab.BrowserProfiles}/profile/${this.profileId}`, - content: profile?.name, - }, - { - content: msg("Configure Profile"), - }, - ]; - } - - const header = { - breadcrumbs, - title: this.profileId ? profile?.name : msg("New Browser Profile"), - secondary: when( - this.profileId - ? { - ...profile, - ...this.config, - } - : this.config, - badges, - badgesSkeleton, - ), - actions: html` { - this.dispatchEvent( - new CustomEvent( - "btrix-user-guide-show", - { - detail: { path: "browser-profiles" }, - bubbles: true, - composed: true, - }, - ), - ); - }} - > - - ${msg("User Guide")} - `, - border: false, - } satisfies Parameters[0]; - - return html` - ${page(header, this.renderPage)} - - (this.isDialogVisible = false)} - > - ${this.renderForm()} - - - - ${msg( - "Are you sure you want to discard changes to this browser profile?", - )} -
    - void this.discardDialog?.hide()} - > - ${msg("No, Continue Browsing")} - - { - void this.discardDialog?.hide(); - this.closeBrowser(); - }} - >${msg("Yes, Cancel")} - -
    -
    - `; - } - - private readonly renderPage = () => { - if (!this.browserId) - return html` - ${msg("Invalid browser")} - `; - - return html`
    - (this.isBrowserLoaded = true)} - @btrix-browser-reload=${this.onBrowserReload} - @btrix-browser-error=${this.onBrowserError} - @btrix-browser-connection-change=${this.onBrowserConnectionChange} - > -
    - -
    - ${this.renderBrowserProfileControls()} -
    `; - }; - - private async onBrowserError() { - this.isBrowserLoaded = false; - } - - private async onBrowserConnectionChange( - e: CustomEvent, - ) { - this.isBrowserLoaded = e.detail.connected; - } - - private onCancel() { - if (!this.isBrowserLoaded) { - this.closeBrowser(); - } else { - void this.discardDialog?.show(); - } - } - - private closeBrowser() { - this.isBrowserLoaded = false; - - if (this.browserId) { - void this.deleteBrowser(this.browserId); - } - - this.navigate.to( - `${this.navigate.orgBasePath}/${OrgTab.BrowserProfiles}${this.profileId ? `/profile/${this.profileId}` : ""}`, - ); - } - - private renderBrowserProfileControls() { - const shouldSave = Boolean(this.profileId); - const disabled = - (shouldSave && !this.profileTask.value) || !this.isBrowserLoaded; - - return html` -
    - - ${msg("Cancel")} - - - { - if (shouldSave && this.profileTask.value) { - void this.saveProfile({ name: this.profileTask.value.name }); - } else { - this.isDialogVisible = true; - } - }} - > - ${this.profileId ? msg("Save Profile") : msg("Finish Browsing")} - - -
    - `; - } - - private renderForm() { - if (this.profileId && !this.profileTask.value) { - return; - } - - const nameValue = this.profileTask.value - ? this.profileTask.value.name - : this.config.name || ""; - - return html`
    -
    - - - - -
    - (this.isDialogVisible = false)} - > - ${msg("Back")} - - - - ${msg("Save Profile")} - -
    -
    -
    `; - } - - private async onBrowserReload() { - const { url } = this.config; - if (!url) { - console.debug("no start url"); - return; - } - - const crawlerChannel = - this.config.crawlerChannel || CrawlerChannelImage.Default; - const proxyId = this.config.proxyId ?? null; - const profileId = this.profileId || undefined; - const data = await this.createBrowser({ - url, - crawlerChannel, - proxyId, - profileId, - }); - - this.navigate.to( - `${this.navigate.orgBasePath}/browser-profiles/profile${this.profileId ? `/${this.profileId}` : ""}/browser/${ - data.browserid - }?${queryString.stringify({ - url, - crawlerChannel, - proxyId, - })}`, - ); - } - - private async onSubmit(e: SubmitEvent) { - e.preventDefault(); - - const formData = new FormData(e.target as HTMLFormElement); - const params = { - name: formData.get("name") as string, - description: formData.get("description") as string, - crawlerChannel: this.config.crawlerChannel, - proxyId: this.config.proxyId, - }; - - await this.saveProfile(params); - } - - private async saveProfile(params: { - name: string; - description?: string; - crawlerChannel?: string; - proxyId?: string | null; - }) { - this.isSubmitting = true; - - try { - let data: { id?: string; updated?: boolean; detail?: string } | undefined; - let retriesLeft = 300; - - while (retriesLeft > 0) { - if (this.profileId) { - data = await this.api.fetch<{ - updated?: boolean; - detail?: string; - }>(`/orgs/${this.orgId}/profiles/${this.profileId}`, { - method: "PATCH", - body: JSON.stringify({ - browserid: this.browserId, - name: params.name, - }), - }); - - if (data.updated !== undefined) { - break; - } - } else { - data = await this.api.fetch<{ id?: string; detail?: string }>( - `/orgs/${this.orgId}/profiles`, - { - method: "POST", - body: JSON.stringify({ - ...params, - browserid: this.browserId, - }), - }, - ); - } - - if (data.id) { - break; - } - if (data.detail === "waiting_for_browser") { - await new Promise((resolve) => setTimeout(resolve, 2000)); - } else { - throw new Error("unknown response"); - } - - retriesLeft -= 1; - } - - if (!retriesLeft) { - throw new Error("too many retries waiting for browser"); - } - - if (!data) { - throw new Error("unknown response"); - } - - this.notify.toast({ - message: msg("Successfully saved browser profile."), - variant: "success", - icon: "check2-circle", - id: "browser-profile-save-status", - }); - - this.navigate.to( - `${this.navigate.orgBasePath}/${OrgTab.BrowserProfiles}/profile/${this.profileId || data.id}`, - ); - } catch (e) { - console.debug(e); - - this.isSubmitting = false; - - 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 update browser profiles.", - ); - } - } - - this.notify.toast({ - message: message, - variant: "danger", - icon: "exclamation-octagon", - id: "browser-profile-save-status", - }); - } - } - - private async createBrowser({ - url, - crawlerChannel, - proxyId, - profileId, - }: { - url: string; - crawlerChannel: string; - proxyId: string | null; - profileId?: string; - }) { - const params = { - url, - crawlerChannel, - proxyId, - profileId, - }; - - return this.api.fetch<{ browserid: string }>( - `/orgs/${this.orgId}/profiles/browser`, - { - method: "POST", - body: JSON.stringify(params), - }, - ); - } - - private async deleteBrowser(id: string) { - try { - const data = await this.api.fetch( - `/orgs/${this.orgId}/profiles/browser/${id}`, - { - method: "DELETE", - }, - ); - - return data; - } catch (err) { - if (isApiError(err) && err.statusCode === 404) { - // Safe to ignore, since unloaded browser will have already been deleted - } else { - console.debug(err); - } - } - } - - private async getProfile(profileId: string, signal: AbortSignal) { - return await this.api.fetch( - `/orgs/${this.orgId}/profiles/${profileId}`, - { signal }, - ); - } -} diff --git a/frontend/src/pages/org/browser-profiles/index.ts b/frontend/src/pages/org/browser-profiles/index.ts index b051d74417..be1f854d48 100644 --- a/frontend/src/pages/org/browser-profiles/index.ts +++ b/frontend/src/pages/org/browser-profiles/index.ts @@ -1,2 +1 @@ -import(/* webpackChunkName: "org" */ "./browser"); import(/* webpackChunkName: "org" */ "./profile"); diff --git a/frontend/src/pages/org/browser-profiles/profile.ts b/frontend/src/pages/org/browser-profiles/profile.ts index f65ea3bb57..96895cfe4c 100644 --- a/frontend/src/pages/org/browser-profiles/profile.ts +++ b/frontend/src/pages/org/browser-profiles/profile.ts @@ -1,19 +1,22 @@ import { consume } from "@lit/context"; import { localized, msg } from "@lit/localize"; import { Task } from "@lit/task"; -import type { SlMenuItem } from "@shoelace-style/shoelace"; -import { html, nothing, type TemplateResult } from "lit"; -import { customElement, property, query, state } from "lit/decorators.js"; +import type { SlButton, SlMenuItem } from "@shoelace-style/shoelace"; +import { serialize } from "@shoelace-style/shoelace/dist/utilities/form.js"; +import { html, type TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; import { ifDefined } from "lit/directives/if-defined.js"; import { when } from "lit/directives/when.js"; import queryString from "query-string"; import { BtrixElement } from "@/classes/BtrixElement"; +import type { Dialog } from "@/components/ui/dialog"; import type { BtrixFilterChipChangeEvent, FilterChip, } from "@/components/ui/filter-chip"; import { parsePage, type PageChangeEvent } from "@/components/ui/pagination"; +import type { UrlInput } from "@/components/ui/url-input"; import { orgCrawlerChannelsContext, type OrgCrawlerChannelsContext, @@ -24,10 +27,6 @@ import { } from "@/context/org-proxies"; import { ClipboardController } from "@/controllers/clipboard"; import { CrawlStatus } from "@/features/archived-items/crawl-status"; -import type { - BrowserConnectionChange, - ProfileBrowser, -} from "@/features/browser-profiles/profile-browser"; import { badges, badgesSkeleton, @@ -87,18 +86,13 @@ export class BrowserProfilesProfilePage extends BtrixElement { | "metadata" | "metadata-name" | "metadata-description" - | "config" - | "browser"; + | "add-site" + | "browser" + | "duplicate"; @state() private initialNavigateUrl?: string; - @state() - private isBrowserLoaded = false; - - @query("btrix-profile-browser") - private readonly profileBrowser?: ProfileBrowser | null; - private get profile() { return this.profileTask.value; } @@ -112,21 +106,6 @@ export class BrowserProfilesProfilePage extends BtrixElement { args: () => [this.profileId] as const, }); - private readonly browserIdTask = new Task(this, { - autoRun: false, - task: async ([profileId, url], { signal }) => { - if (!url) return; - - const { browserid } = await this.createBrowser( - { profileId, url }, - signal, - ); - - return browserid; - }, - args: () => [this.profileId, this.initialNavigateUrl] as const, - }); - private readonly workflowsTask = new Task(this, { task: async ([profileId, workflowParams], { signal }) => { return this.getWorkflows({ ...workflowParams, profileId }, signal); @@ -165,16 +144,40 @@ export class BrowserProfilesProfilePage extends BtrixElement { actions: this.renderActions(), } satisfies Parameters[0]; - const org = this.org; - const proxies = this.orgProxies; - const crawlerChannels = this.orgCrawlerChannels; - const crawlingDefaultsReady = org && proxies && crawlerChannels; + const duplicating = this.openDialog === "duplicate"; return html`${page(header, this.renderPage)} - ${when( - this.profile, - (profile) => - html` void this.profileTask.run()} + @sl-after-hide=${() => { + this.initialNavigateUrl = undefined; + this.openDialog = undefined; + }} + > + + + ${when( + this.profile, + (profile) => + html` - - - ${crawlingDefaultsReady - ? html` (this.openDialog = undefined)} - >` - : nothing} `, - )} `; + `, + )} `; } private renderActions() { const archivingDisabled = isArchivingDisabled(this.org); + const isCrawler = this.appState.isCrawler; const menuItemClick = (cb: () => void) => (e: MouseEvent) => { if (e.defaultPrevented || (e.currentTarget as SlMenuItem).disabled) return; @@ -218,29 +207,25 @@ export class BrowserProfilesProfilePage extends BtrixElement { }; return html` - this.openBrowser()}> - - ${msg("View Profile")} - ${msg("Actions")} ${when( - this.appState.isCrawler, + isCrawler, () => html` + (this.openDialog = "metadata")}> + + ${msg("Edit Metadata")} + (this.openDialog = "config"))} + @click=${menuItemClick(() => void this.openBrowser())} > ${msg("Configure Profile")} - (this.openDialog = "metadata")}> - - ${msg("Edit Metadata")} - void this.duplicateProfile())} @@ -257,7 +242,7 @@ export class BrowserProfilesProfilePage extends BtrixElement { ${msg("Copy Profile ID")} - ${when(this.appState.isCrawler, () => { + ${when(isCrawler, () => { const disabled = this.profile?.inUse; return html` @@ -294,86 +279,128 @@ export class BrowserProfilesProfilePage extends BtrixElement { }; private renderProfile() { - const readyBrowserId = - this.openDialog === "browser" && - this.profile && - this.initialNavigateUrl && - this.browserIdTask.value; + const archivingDisabled = isArchivingDisabled(this.org); + const isCrawler = this.appState.isCrawler; return panel({ heading: msg("Configured Sites"), - actions: this.appState.isCrawler - ? html` (this.openDialog = "config")} - > - - ${msg("Configure Profile")} - ` + actions: isCrawler + ? html` + void this.openBrowser()} + ?disabled=${archivingDisabled} + > + ` : undefined, body: html`${this.renderOrigins()} + ${when( + isCrawler, + () => html` + (this.openDialog = "add-site")} + > + + ${msg("Add Site")} + `, + )} + ${this.renderAddSiteDialog()} `, + }); + } - this.closeBrowser()} + private renderAddSiteDialog() { + return html` { + const dialog = e.target as Dialog; + await this.updateComplete; + dialog.querySelector("btrix-url-input")?.focus(); + }} + @sl-after-hide=${async (e: CustomEvent) => { + const dialog = e.target as Dialog; + const form = dialog.querySelector("form"); + const input = dialog.querySelector("btrix-url-input"); + + if (form) { + form.reset(); + } + + if (input) { + input.value = ""; + input.setCustomValidity(""); + } + + if (this.openDialog === "add-site") { + this.openDialog = undefined; + } + }} + > +
    { + e.preventDefault(); + + const form = e.target as HTMLFormElement; + + if (!form.checkValidity()) return; + + const values = serialize(form); + const url = values["starting-url"] as string; + + void this.openBrowser(url); + }} + > + +
    +
    + (this.openDialog = undefined)} + >${msg("Cancel")} - this.profileBrowser?.toggleOrigins()} - > - ${readyBrowserId - ? html` ` - : html`
    `} -
    - ${msg("View Only")} - ${msg("Browsing history will not be saved to profile.")} -
    - `, - }); + { + const button = e.target as SlButton; + const dialog = button.closest("btrix-dialog"); + dialog?.submit(); + }} + > + ${msg("Start Browser")} + +
    +
    `; } private renderOrigins() { - const originsSkeleton = () => html`
    `; + const originsSkeleton = () => + html`
    `; const origins = (profile: Profile) => profile.origins.map( (origin) => html`
  • - - - +
    - + html` -
      - ${origins(profile)} -
    +
    +
      + ${origins(profile)} +
    +
    `, originsSkeleton, ); @@ -449,7 +478,7 @@ export class BrowserProfilesProfilePage extends BtrixElement { this.localize.bytes(profile.resource?.size || 0), )} - + ${this.renderDetail( (profile) => `${this.localize.number(profile.origins.length)} ${pluralOf("domains", profile.origins.length)}`, @@ -714,41 +743,22 @@ export class BrowserProfilesProfilePage extends BtrixElement { () => html``, ); - private readonly openBrowser = () => { - if (!this.profile) return; - - this.initialNavigateUrl = - this.initialNavigateUrl || this.profile.origins[0]; + private async getFirstBrowserUrl() { + if (!this.profile) { + await this.profileTask.taskComplete; + } - void this.browserIdTask.run(); + return this.profile?.origins[0]; + } + private readonly openBrowser = async (url?: string) => { + if (!url) { + url = await this.getFirstBrowserUrl(); + } + this.initialNavigateUrl = url; this.openDialog = "browser"; }; - private readonly closeBrowser = () => { - this.initialNavigateUrl = undefined; - this.openDialog = undefined; - }; - - private readonly onBrowserLoad = () => { - this.isBrowserLoaded = true; - }; - - private readonly onBrowserReload = () => { - this.isBrowserLoaded = false; - void this.browserIdTask.run(); - }; - - private readonly onBrowserError = () => { - this.isBrowserLoaded = false; - }; - - private readonly onBrowserConnectionChange = ( - e: CustomEvent, - ) => { - this.isBrowserLoaded = e.detail.connected; - }; - private async getProfile(profileId: string, signal: AbortSignal) { return await this.api.fetch( `/orgs/${this.orgId}/profiles/${profileId}`, @@ -757,46 +767,8 @@ export class BrowserProfilesProfilePage extends BtrixElement { } private async duplicateProfile() { - if (!this.profile) { - console.debug("missing profile"); - return; - } - - const profile = this.profile; - const url = profile.origins[0]; - - try { - const data = await this.createBrowser({ - url, - }); - - this.notify.toast({ - message: msg("Starting up browser..."), - variant: "success", - icon: "check2-circle", - id: "browser-profile-status", - }); - - this.navigate.to( - `${this.navigate.orgBasePath}/browser-profiles/profile/browser/${ - data.browserid - }?${queryString.stringify({ - url, - name: `${profile.name} ${msg("Copy")}`, - crawlerChannel: profile.crawlerChannel, - proxyId: profile.proxyId, - })}`, - ); - } catch (e) { - console.debug(e); - - this.notify.toast({ - message: msg("Sorry, something went wrong starting up browser."), - variant: "danger", - icon: "exclamation-octagon", - id: "browser-profile-status", - }); - } + this.initialNavigateUrl = await this.getFirstBrowserUrl(); + this.openDialog = "duplicate"; } private async deleteProfile() { @@ -856,7 +828,7 @@ export class BrowserProfilesProfilePage extends BtrixElement { } private async createBrowser( - params: { url: string; profileId?: string }, + params: { url: string; profileId: string }, signal?: AbortSignal, ) { return this.api.fetch<{ browserid: string }>( diff --git a/frontend/src/pages/org/index.ts b/frontend/src/pages/org/index.ts index 2fe13688f8..7ecb624afd 100644 --- a/frontend/src/pages/org/index.ts +++ b/frontend/src/pages/org/index.ts @@ -489,14 +489,14 @@ export class Org extends BtrixElement { > ${crawlingDefaultsReady - ? html` (this.openDialogName = undefined)} > - ` + ` : nothing} { const params = this.params as OrgParams["browser-profiles"]; - if (params.browserId) { - return html``; - } - if (params.profileId) { return html` Date: Tue, 11 Nov 2025 08:34:20 -0800 Subject: [PATCH 20/27] improve loading and teardown states --- .../profile-browser-dialog.ts | 5 +-- .../browser-profiles/profile-browser.ts | 34 ++++++++++++++++--- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/frontend/src/features/browser-profiles/profile-browser-dialog.ts b/frontend/src/features/browser-profiles/profile-browser-dialog.ts index e70fd15398..a1ccc4b722 100644 --- a/frontend/src/features/browser-profiles/profile-browser-dialog.ts +++ b/frontend/src/features/browser-profiles/profile-browser-dialog.ts @@ -56,9 +56,10 @@ export class ProfileBrowserDialog extends BtrixElement { task: async ([open, config, profile], { signal }) => { if (!open || !config) return null; - if (this.browserIdTask.value) { + const browserId = this.browserIdTask.value; + if (browserId && browserId !== this.#savedBrowserId) { // Delete previously created and unused browser - void this.deleteBrowser(this.browserIdTask.value, signal); + void this.deleteBrowser(browserId, signal); } const { browserid } = await this.createBrowser( diff --git a/frontend/src/features/browser-profiles/profile-browser.ts b/frontend/src/features/browser-profiles/profile-browser.ts index cd416ba8e8..73a5ecc771 100644 --- a/frontend/src/features/browser-profiles/profile-browser.ts +++ b/frontend/src/features/browser-profiles/profile-browser.ts @@ -210,8 +210,7 @@ export class ProfileBrowser extends BtrixElement { } private readonly onBeforeUnload = (e: BeforeUnloadEvent) => { - console.log("e", e); - // e.preventDefault(); + e.preventDefault(); }; private readonly onBrowserError = async () => { @@ -260,14 +259,39 @@ export class ProfileBrowser extends BtrixElement { } render() { + const loadingMsgChangeDelay = 8000; const browserLoading = () => cache( html`
    -

    - ${msg("Loading interactive browser...")} -

    +
    + +

    + ${msg("Loading interactive browser...")} +

    +
    + +

    + ${msg("Browser is still warming up...")} +

    +
    +
    + Date: Tue, 11 Nov 2025 08:54:08 -0800 Subject: [PATCH 21/27] include any failed --- frontend/src/pages/org/browser-profiles/profile.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/frontend/src/pages/org/browser-profiles/profile.ts b/frontend/src/pages/org/browser-profiles/profile.ts index 96895cfe4c..fc16013047 100644 --- a/frontend/src/pages/org/browser-profiles/profile.ts +++ b/frontend/src/pages/org/browser-profiles/profile.ts @@ -633,7 +633,10 @@ export class BrowserProfilesProfilePage extends BtrixElement { private readonly renderWorkflows = ( workflows: APIPaginatedList, ) => { - const failedNotLoggedInState = "failed_not_logged_in" satisfies CrawlState; + const failedStates = [ + "failed", + "failed_not_logged_in", + ] satisfies CrawlState[]; return html`
    @@ -643,19 +646,19 @@ export class BrowserProfilesProfilePage extends BtrixElement { + (failedStates as CrawlState[]).includes(state), )} @btrix-change=${(e: BtrixFilterChipChangeEvent) => { const { checked } = e.target as FilterChip; this.workflowParams = { ...this.workflowParams, - lastCrawlState: checked ? [failedNotLoggedInState] : undefined, + lastCrawlState: checked ? failedStates : undefined, }; }} > - ${CrawlStatus.getContent({ state: failedNotLoggedInState }).label} + ${CrawlStatus.getContent({ state: "failed" }).label}
    From c0a9f9ae3dcaf7a921731319967ac72a5f3c9caa Mon Sep 17 00:00:00 2001 From: sua yoo Date: Tue, 11 Nov 2025 09:03:24 -0800 Subject: [PATCH 22/27] add to action menu --- .../crawl-workflows/workflow-editor.ts | 12 +++- .../src/pages/org/browser-profiles/profile.ts | 70 ++++++++----------- 2 files changed, 38 insertions(+), 44 deletions(-) diff --git a/frontend/src/features/crawl-workflows/workflow-editor.ts b/frontend/src/features/crawl-workflows/workflow-editor.ts index e77dea1993..c8ef39aec2 100644 --- a/frontend/src/features/crawl-workflows/workflow-editor.ts +++ b/frontend/src/features/crawl-workflows/workflow-editor.ts @@ -83,7 +83,7 @@ import type { import { infoCol, inputCol } from "@/layouts/columns"; import { pageSectionsWithNav } from "@/layouts/pageSectionsWithNav"; import { panel } from "@/layouts/panel"; -import { WorkflowTab } from "@/routes"; +import { OrgTab, WorkflowTab } from "@/routes"; import { infoTextFor } from "@/strings/crawl-workflows/infoText"; import { labelFor } from "@/strings/crawl-workflows/labels"; import scopeTypeLabels from "@/strings/crawl-workflows/scopeType"; @@ -3137,10 +3137,16 @@ https://archiveweb.page/images/${"logo.svg"}`} } private async onReset() { + const fromBrowserProfile = + !this.initialSeeds && + !this.initialSeedFile && + this.initialWorkflow?.profileid; + this.navigate.to( - `${this.navigate.orgBasePath}/workflows${this.configId ? `/${this.configId}/${WorkflowTab.Settings}` : ""}`, + fromBrowserProfile + ? `${this.navigate.orgBasePath}/${OrgTab.BrowserProfiles}/profile/${fromBrowserProfile}` + : `${this.navigate.orgBasePath}/${OrgTab.Workflows}${this.configId ? `/${this.configId}/${WorkflowTab.Settings}` : ""}`, ); - // this.initializeEditor(); } private validateUrlList( diff --git a/frontend/src/pages/org/browser-profiles/profile.ts b/frontend/src/pages/org/browser-profiles/profile.ts index fc16013047..87f2f4c1b0 100644 --- a/frontend/src/pages/org/browser-profiles/profile.ts +++ b/frontend/src/pages/org/browser-profiles/profile.ts @@ -234,6 +234,11 @@ export class BrowserProfilesProfilePage extends BtrixElement { ${msg("Duplicate Profile")} + + + ${msg("New Workflow with Profile")} + + `, )} ${msg("Manage Workflows")} - { - this.navigate.to( - `${this.navigate.orgBasePath}/${OrgTab.Workflows}/new#${"browserSettings" satisfies SectionsEnum}`, - settingsForDuplicate({ - workflow: { - profileid: this.profileId, - proxyId: profile.proxyId, - crawlerChannel: profile.crawlerChannel, - }, - }), - ); - - this.notify.toast({ - message: msg( - "Copied browser settings to new workflow.", - ), - variant: "success", - icon: "check2-circle", - id: "workflow-copied-status", - duration: 8000, - }); - }} - > + - ${msg("New Workflow with Profile")} + ${msg("New Workflow with Profile")} +
    `, }), }), @@ -754,6 +735,27 @@ export class BrowserProfilesProfilePage extends BtrixElement { return this.profile?.origins[0]; } + private readonly newWorkflow = () => { + this.navigate.to( + `${this.navigate.orgBasePath}/${OrgTab.Workflows}/new#${"browserSettings" satisfies SectionsEnum}`, + settingsForDuplicate({ + workflow: { + profileid: this.profileId, + proxyId: this.profile?.proxyId, + crawlerChannel: this.profile?.crawlerChannel, + }, + }), + ); + + this.notify.toast({ + message: msg("Copied browser settings to new workflow."), + variant: "success", + icon: "check2-circle", + id: "workflow-copied-status", + duration: 8000, + }); + }; + private readonly openBrowser = async (url?: string) => { if (!url) { url = await this.getFirstBrowserUrl(); @@ -830,20 +832,6 @@ export class BrowserProfilesProfilePage extends BtrixElement { } } - private async createBrowser( - params: { url: string; profileId: string }, - signal?: AbortSignal, - ) { - return this.api.fetch<{ browserid: string }>( - `/orgs/${this.orgId}/profiles/browser`, - { - method: "POST", - body: JSON.stringify(params), - signal, - }, - ); - } - private async getWorkflows( params: { profileId: string } & APIPaginationQuery, signal: AbortSignal, From f41193292d25fe0ca9328d92f445985e5bec0836 Mon Sep 17 00:00:00 2001 From: sua yoo Date: Tue, 11 Nov 2025 10:50:23 -0800 Subject: [PATCH 23/27] fix document title --- frontend/src/pages/org/browser-profiles/profile.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/frontend/src/pages/org/browser-profiles/profile.ts b/frontend/src/pages/org/browser-profiles/profile.ts index 87f2f4c1b0..c3eddd364e 100644 --- a/frontend/src/pages/org/browser-profiles/profile.ts +++ b/frontend/src/pages/org/browser-profiles/profile.ts @@ -124,22 +124,18 @@ export class BrowserProfilesProfilePage extends BtrixElement { content: this.profile?.name, }, ], - title: html`${this.profile?.name ?? - html``} - ${when( + title: this.profile?.name, + suffix: when( this.profile && this.appState.isCrawler, () => html` (this.openDialog = "metadata-name")} > `, - )} `, + ), secondary: when(this.profile, badges, badgesSkeleton), actions: this.renderActions(), } satisfies Parameters[0]; From 4c9dc921d81425506f751df1bb8ee5cb93b7830f Mon Sep 17 00:00:00 2001 From: sua yoo Date: Tue, 11 Nov 2025 11:27:50 -0800 Subject: [PATCH 24/27] export class name --- .../features/browser-profiles/profile-browser-dialog.ts | 9 +++++---- .../src/features/browser-profiles/profile-browser.ts | 9 +++++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/frontend/src/features/browser-profiles/profile-browser-dialog.ts b/frontend/src/features/browser-profiles/profile-browser-dialog.ts index a1ccc4b722..caa3e5ff83 100644 --- a/frontend/src/features/browser-profiles/profile-browser-dialog.ts +++ b/frontend/src/features/browser-profiles/profile-browser-dialog.ts @@ -10,9 +10,10 @@ import { badges } from "./templates/badges"; import { BtrixElement } from "@/classes/BtrixElement"; import type { Dialog } from "@/components/ui/dialog"; -import type { - BrowserConnectionChange, - ProfileBrowser, +import { + bgClass, + type BrowserConnectionChange, + type ProfileBrowser, } from "@/features/browser-profiles/profile-browser"; import type { CreateBrowserOptions, @@ -222,7 +223,7 @@ export class ProfileBrowserDialog extends BtrixElement {
  • -
    +
    ${this.browserIdTask.render({ complete: (browserId) => browserId diff --git a/frontend/src/features/browser-profiles/profile-browser.ts b/frontend/src/features/browser-profiles/profile-browser.ts index 73a5ecc771..4f88f643a5 100644 --- a/frontend/src/features/browser-profiles/profile-browser.ts +++ b/frontend/src/features/browser-profiles/profile-browser.ts @@ -8,6 +8,11 @@ import { when } from "lit/directives/when.js"; import { BtrixElement } from "@/classes/BtrixElement"; import { emptyMessage } from "@/layouts/emptyMessage"; import { isApiError, type APIError } from "@/utils/api"; +import { tw } from "@/utils/tailwind"; + +// Matches background of embedded browser +// TODO See if this can be configurable via API +export const bgClass = tw`bg-[#282828]`; const POLL_INTERVAL_SECONDS = 2; const hiddenClassList = ["translate-x-2/3", "opacity-0", "pointer-events-none"]; @@ -259,7 +264,7 @@ export class ProfileBrowser extends BtrixElement { } render() { - const loadingMsgChangeDelay = 8000; + const loadingMsgChangeDelay = 10000; const browserLoading = () => cache( html`
    ${this.renderControlBar()} From 79acfc167ba5b01b740efc337e8e90e988ddbafe Mon Sep 17 00:00:00 2001 From: sua yoo Date: Tue, 11 Nov 2025 12:33:09 -0800 Subject: [PATCH 25/27] update modified fields --- .../src/pages/org/browser-profiles-list.ts | 17 ++- .../src/pages/org/browser-profiles/profile.ts | 130 ++++++++++++------ 2 files changed, 100 insertions(+), 47 deletions(-) diff --git a/frontend/src/pages/org/browser-profiles-list.ts b/frontend/src/pages/org/browser-profiles-list.ts index f89165a3df..f31682852b 100644 --- a/frontend/src/pages/org/browser-profiles-list.ts +++ b/frontend/src/pages/org/browser-profiles-list.ts @@ -32,7 +32,7 @@ import { isArchivingDisabled } from "@/utils/orgs"; const SORT_DIRECTIONS = ["asc", "desc"] as const; type SortDirection = (typeof SORT_DIRECTIONS)[number]; -type SortField = "name" | "url" | "modified"; +type SortField = "name" | "url" | "modified" | "created"; type SortBy = { field: SortField; direction: SortDirection; @@ -51,7 +51,11 @@ const sortableFields: Record< defaultDirection: "asc", }, modified: { - label: msg("Last Modified"), + label: msg("Modified By User"), + defaultDirection: "desc", + }, + created: { + label: msg("Date Created"), defaultDirection: "desc", }, }; @@ -403,6 +407,11 @@ export class BrowserProfilesList extends BtrixElement { }; private readonly renderItem = (data: Profile) => { + const modifiedByAnyDate = + [data.modifiedCrawlDate, data.modified, data.created].reduce( + (a, b) => (b && a && b > a ? b : a), + data.created, + ) || data.created; const startingUrl = data.origins[0]; const otherOrigins = data.origins.slice(1); @@ -443,9 +452,7 @@ export class BrowserProfilesList extends BtrixElement { : nothing} - ${this.localize.relativeDate(data.modified || data.created, { - capitalize: true, - })} + ${this.localize.relativeDate(modifiedByAnyDate)} ${this.renderActions(data)} diff --git a/frontend/src/pages/org/browser-profiles/profile.ts b/frontend/src/pages/org/browser-profiles/profile.ts index c3eddd364e..d8c68c4324 100644 --- a/frontend/src/pages/org/browser-profiles/profile.ts +++ b/frontend/src/pages/org/browser-profiles/profile.ts @@ -220,7 +220,7 @@ export class BrowserProfilesProfilePage extends BtrixElement { @click=${menuItemClick(() => void this.openBrowser())} > - ${msg("Configure Profile")} + ${msg("Edit Configuration")} + ? html` ${stringFor.none}`; + const profile = this.profile; + const modifiedByAnyDate = profile + ? [profile.modifiedCrawlDate, profile.modified, profile.created].reduce( + (a, b) => (b && a && b > a ? b : a), + profile.created, + ) + : null; return panel({ heading: msg("Overview"), @@ -487,35 +494,67 @@ export class BrowserProfilesProfilePage extends BtrixElement { ${this.renderDetail((profile) => - this.localize.relativeDate( - // NOTE older profiles may not have "modified" data - profile.modified || profile.created, - ), + this.localize.relativeDate(modifiedByAnyDate || profile.created), )} - ${when(this.profile, (profile) => - profile.modified - ? html` - ${this.renderDetail((profile) => { - if ( - profile.modifiedCrawlDate && - (!profile.modified || - profile.modifiedCrawlDate >= profile.modified) - ) { - return msg("Automatic update from crawl"); - } - - if (profile.modifiedByName) { - return profile.modifiedByName; - } - - return noData; - })} - ` - : html` - ${profile.createdByName || noData} - `, + + ${this.renderDetail((profile) => { + let modifier: string | TemplateResult = + profile.createdByName || msg("User"); + + switch (modifiedByAnyDate) { + case profile.modifiedCrawlDate: + modifier = html` + ${msg("Workflow Crawl")} + `; + break; + case profile.modified: + modifier = profile.modifiedByName || modifier; + break; + default: + break; + } + + return modifier; + })} + + ${when( + this.profile, + (profile) => + modifiedByAnyDate === profile.modifiedCrawlDate + ? html` + ${profile.modifiedByName} + (${this.localize.relativeDate( + profile.modified || profile.created, + )}) + ` + : html` + ${this.localize.date(profile.created, { + dateStyle: "medium", + })} + `, + () => + html` + + + `, )} + + + ${this.renderDetail((profile) => + profile.created + ? this.localize.date(profile.created, { + dateStyle: "medium", + }) + : noData, + )} + ${this.renderDetail((profile) => { const isBackedUp = @@ -552,22 +591,29 @@ export class BrowserProfilesProfilePage extends BtrixElement { ? `${this.localize.number(this.workflowsTask.value.total)} ${pluralOf("workflows", this.workflowsTask.value.total)}` : html``} - + ${profile.modifiedCrawlId && profile.modifiedCrawlDate ? html` - ${this.localize.relativeDate( - profile.modifiedCrawlDate, - )} - - ${msg("View Crawl")} +
    + ${this.localize.relativeDate( + profile.modifiedCrawlDate, + )} + + + +
    ` - : msg("No")} + : msg("Never")}
    @@ -720,7 +766,7 @@ export class BrowserProfilesProfilePage extends BtrixElement { when( this.profile, render, - () => html``, + () => html``, ); private async getFirstBrowserUrl() { From 7554d930297bb583d6349f5ea9ef2645cfaee63d Mon Sep 17 00:00:00 2001 From: sua yoo Date: Tue, 11 Nov 2025 19:57:10 -0800 Subject: [PATCH 26/27] enable choosing crawler channel --- .../profile-browser-dialog.ts | 5 +- .../src/pages/org/browser-profiles/profile.ts | 101 ++++++++++++------ 2 files changed, 73 insertions(+), 33 deletions(-) diff --git a/frontend/src/features/browser-profiles/profile-browser-dialog.ts b/frontend/src/features/browser-profiles/profile-browser-dialog.ts index caa3e5ff83..36b94e96b9 100644 --- a/frontend/src/features/browser-profiles/profile-browser-dialog.ts +++ b/frontend/src/features/browser-profiles/profile-browser-dialog.ts @@ -176,7 +176,10 @@ export class ProfileBrowserDialog extends BtrixElement { .join(" — ")} ${when( - creatingNew ? this.config : this.profile || this.config, + (this.profile || this.config) && { + ...this.profile, + ...this.config, + }, badges, )}
    diff --git a/frontend/src/pages/org/browser-profiles/profile.ts b/frontend/src/pages/org/browser-profiles/profile.ts index d8c68c4324..f42d024351 100644 --- a/frontend/src/pages/org/browser-profiles/profile.ts +++ b/frontend/src/pages/org/browser-profiles/profile.ts @@ -86,12 +86,16 @@ export class BrowserProfilesProfilePage extends BtrixElement { | "metadata" | "metadata-name" | "metadata-description" - | "add-site" + | "start-browser" + | "start-browser-add-site" | "browser" | "duplicate"; @state() - private initialNavigateUrl?: string; + private browserLoadUrl?: string; + + @state() + private browserLoadCrawlerChannel?: Profile["crawlerChannel"]; private get profile() { return this.profileTask.value; @@ -146,15 +150,16 @@ export class BrowserProfilesProfilePage extends BtrixElement { void this.profileTask.run()} - @sl-after-hide=${() => { - this.initialNavigateUrl = undefined; - this.openDialog = undefined; - }} + @sl-after-hide=${this.closeBrowser} > @@ -217,10 +219,12 @@ export class BrowserProfilesProfilePage extends BtrixElement { void this.openBrowser())} + @click=${menuItemClick( + () => (this.openDialog = "start-browser"), + )} > - ${msg("Edit Configuration")} + ${msg("Configure Profile")} + ? html` void this.openBrowser()} + @click=${() => (this.openDialog = "start-browser")} ?disabled=${archivingDisabled} > ` @@ -302,25 +306,49 @@ export class BrowserProfilesProfilePage extends BtrixElement { (this.openDialog = "add-site")} + @click=${() => (this.openDialog = "start-browser-add-site")} > ${msg("Add Site")} `, )} - ${this.renderAddSiteDialog()} `, + ${this.renderStartBrowserDialog()} `, }); } - private renderAddSiteDialog() { + private renderStartBrowserDialog() { + const fields = (profile: Profile) => { + const value = this.openDialog === "start-browser" && profile.origins[0]; + + return html` + + + ${when( + this.orgCrawlerChannels && this.orgCrawlerChannels.length > 1, + () => html` +
    + + +
    + `, + )} + `; + }; return html` { - const dialog = e.target as Dialog; - await this.updateComplete; - dialog.querySelector("btrix-url-input")?.focus(); + if (this.openDialog === "start-browser-add-site") { + const dialog = e.target as Dialog; + await this.updateComplete; + dialog.querySelector("btrix-url-input")?.focus(); + } }} @sl-after-hide=${async (e: CustomEvent) => { const dialog = e.target as Dialog; @@ -336,7 +364,7 @@ export class BrowserProfilesProfilePage extends BtrixElement { input.setCustomValidity(""); } - if (this.openDialog === "add-site") { + if (this.openDialog?.startsWith("start-browser")) { this.openDialog = undefined; } }} @@ -351,15 +379,12 @@ export class BrowserProfilesProfilePage extends BtrixElement { const values = serialize(form); const url = values["starting-url"] as string; + const crawlerChannel = values["crawlerChannel"] as string | undefined; - void this.openBrowser(url); + void this.openBrowser({ url, crawlerChannel }); }} > - + ${when(this.profileTask.value, fields)}
    (this.openDialog = undefined)} @@ -390,7 +415,7 @@ export class BrowserProfilesProfilePage extends BtrixElement {