+ ${originsWithRemainder(profile.origins, {
+ disablePopover: true,
+ })}
+
+
+
+ `;
+
+ const profiles = browserProfiles.items;
+ const priorityOrigins = this.suggestOrigins;
+ const suggestions: Profile[] = [];
+ let rest: Profile[] = [];
+
+ if (priorityOrigins?.length) {
+ profiles.forEach((profile) => {
+ const { origins } = profile;
+ if (
+ origins.some((origin) =>
+ priorityOrigins.includes(
+ new URL(origin).hostname.replace(/^www\./, ""),
+ ),
+ )
+ ) {
+ suggestions.push(profile);
+ } else {
+ rest.push(profile);
+ }
+ });
+ } else {
+ rest = profiles;
+ }
+
+ return html`
+
@@ -194,10 +398,9 @@ export class SelectBrowserProfile extends BtrixElement {
`;
}
- private async onChange(e: Event) {
- this.selectedProfile = this.browserProfiles?.find(
- ({ id }) => id === (e.target as SlSelect | null)?.value,
- );
+ private async onChange(e: SlChangeEvent) {
+ const profileId = (e.target as SlSelect | null)?.value as string;
+ this.selectedProfile = this.findProfileById(profileId);
await this.updateComplete;
@@ -210,43 +413,30 @@ export class SelectBrowserProfile extends BtrixElement {
);
}
- private async updateSelectedProfile() {
- await this.fetchBrowserProfiles();
- await this.updateComplete;
-
- if (this.profileId && !this.selectedProfile) {
- this.selectedProfile = this.browserProfiles?.find(
- ({ id }) => id === this.profileId,
- );
- }
- }
-
- /**
- * Fetch browser profiles and update internal state
- */
- private async fetchBrowserProfiles(): Promise
{
- try {
- const data = await this.getProfiles();
-
- this.browserProfiles = orderBy(["name", "modified"])(["asc", "desc"])(
- data,
- ) as Profile[];
- } catch (e) {
- this.notify.toast({
- message: msg("Sorry, couldn't retrieve browser profiles at this time."),
- variant: "danger",
- icon: "exclamation-octagon",
- id: "browser-profile-status",
- });
- }
- }
+ private async getProfiles(
+ params: {
+ userid?: string;
+ tags?: string[];
+ tagMatch?: string;
+ } & APIPaginationQuery &
+ APISortQuery,
+ signal: AbortSignal,
+ ) {
+ const query = queryString.stringify(
+ {
+ ...params,
+ },
+ {
+ arrayFormat: "none", // For tags
+ },
+ );
- private async getProfiles() {
const data = await this.api.fetch>(
- `/orgs/${this.orgId}/profiles`,
+ `/orgs/${this.orgId}/profiles?${query}`,
+ { signal },
);
- return data.items;
+ return data;
}
/**
diff --git a/frontend/src/features/browser-profiles/start-browser-dialog.ts b/frontend/src/features/browser-profiles/start-browser-dialog.ts
index 877f15acd3..a9b9fa2a11 100644
--- a/frontend/src/features/browser-profiles/start-browser-dialog.ts
+++ b/frontend/src/features/browser-profiles/start-browser-dialog.ts
@@ -230,41 +230,33 @@ export class StartBrowserDialog extends BtrixElement {
)}
- ${when(
- this.open && (showChannels || showProxies),
- () => html`
-
- ${msg("Crawler Settings")}
-
- ${showChannels
- ? html`
-
-
-
`
- : nothing}
- ${showProxies
- ? html`
-
-
-
`
- : nothing}
-
- `,
- )}
+ ${showProxies
+ ? html`
+
+
+
`
+ : nothing}
+ ${this.open && showChannels
+ ? html`
+ ${msg("Browser Session Settings")}
+
+
+
+
+ `
+ : nothing}
void this.dialog?.hide()}
@@ -318,7 +310,7 @@ export class StartBrowserDialog extends BtrixElement {
${msg("Suggestions from Related Workflows")}
- ${seeds.map(option)}
+ ${seeds.slice(0, 10).map(option)}
`
: nothing,
)}
diff --git a/frontend/src/features/browser-profiles/templates/badges.ts b/frontend/src/features/browser-profiles/templates/badges.ts
index 303b2810dd..5fdea74f6f 100644
--- a/frontend/src/features/browser-profiles/templates/badges.ts
+++ b/frontend/src/features/browser-profiles/templates/badges.ts
@@ -1,12 +1,15 @@
import { msg } from "@lit/localize";
import { html, nothing } from "lit";
import { when } from "lit/directives/when.js";
-import capitalize from "lodash/fp/capitalize";
-import { CrawlerChannelImage, type Profile } from "@/types/crawler";
+import { type Profile } from "@/types/crawler";
export const usageBadge = (inUse: boolean) =>
- html`
+ html`
- html`
-
-
- ${capitalize(channel)}
-
- `,
+ (channelImage) => html`
+
+ `,
)}
${when(
profile.proxyId,
- (proxy) =>
- html`
-
-
- ${proxy}
-
- `,
+ (proxyId) => html`
+
+ `,
)}
`;
};
diff --git a/frontend/src/features/browser-profiles/templates/origins-with-remainder.ts b/frontend/src/features/browser-profiles/templates/origins-with-remainder.ts
new file mode 100644
index 0000000000..17080a26d9
--- /dev/null
+++ b/frontend/src/features/browser-profiles/templates/origins-with-remainder.ts
@@ -0,0 +1,39 @@
+import { html, nothing } from "lit";
+
+import type { Profile } from "@/types/crawler";
+import localize from "@/utils/localize";
+
+/**
+ * Displays primary origin with remainder in a popover badge
+ */
+export function originsWithRemainder(
+ origins: Profile["origins"],
+ { disablePopover } = { disablePopover: false },
+) {
+ const startingUrl = origins[0];
+ const otherOrigins = origins.slice(1);
+
+ return html`
+
+ ${otherOrigins.length
+ ? html`
+
+ +${localize.number(otherOrigins.length)}
+
+ ${otherOrigins.map((url) => html`- ${url}
`)}
+
+
+ `
+ : nothing}
+
`;
+}
diff --git a/frontend/src/features/crawl-workflows/workflow-editor.ts b/frontend/src/features/crawl-workflows/workflow-editor.ts
index 68942af1eb..28a0df4d48 100644
--- a/frontend/src/features/crawl-workflows/workflow-editor.ts
+++ b/frontend/src/features/crawl-workflows/workflow-editor.ts
@@ -1,5 +1,6 @@
import { consume } from "@lit/context";
import { localized, msg, str } from "@lit/localize";
+import { Task } from "@lit/task";
import type {
SlBlurEvent,
SlChangeEvent,
@@ -33,6 +34,7 @@ import {
state,
} from "lit/decorators.js";
import { choose } from "lit/directives/choose.js";
+import { guard } from "lit/directives/guard.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { map } from "lit/directives/map.js";
import { when } from "lit/directives/when.js";
@@ -95,6 +97,7 @@ import {
Behavior,
CrawlerChannelImage,
ScopeType,
+ type Profile,
type Seed,
type WorkflowParams,
} from "@/types/crawler";
@@ -419,6 +422,15 @@ export class WorkflowEditor extends BtrixElement {
// https://github.com/webrecorder/browsertrix-crawler/blob/v1.5.8/package.json#L23
private readonly cssParser = createParser();
+ private readonly profileTask = new Task(this, {
+ task: async ([formState], { signal }) => {
+ if (!formState.browserProfile) return;
+
+ return this.getProfile(formState.browserProfile.id, signal);
+ },
+ args: () => [this.formState] as const,
+ });
+
connectedCallback(): void {
this.initializeEditor();
super.connectedCallback();
@@ -1982,15 +1994,49 @@ https://archiveweb.page/images/${"logo.svg"}`}
if (!this.formState.lang) throw new Error("missing formstate.lang");
const proxies = this.proxies;
+ const profileProxyId =
+ this.formState.browserProfile?.proxyId ||
+ (this.formState.browserProfile?.id && this.formState.proxyId);
+
+ const priorityOrigins = () => {
+ if (!this.formState.urlList && !this.formState.primarySeedUrl) {
+ return [];
+ }
+
+ const crawlUrls = urlListToArray(this.formState.urlList);
+
+ if (this.formState.primarySeedUrl) {
+ crawlUrls.unshift(this.formState.primarySeedUrl);
+ }
+
+ return crawlUrls
+ .map((url) => {
+ try {
+ return new URL(url).hostname.replace(/^www\./, "");
+ } catch {
+ return "";
+ }
+ })
+ .filter((url) => url);
+ };
return html`
${inputCol(html`
+ .profileName=${this.formState.browserProfile?.name}
+ .suggestOrigins=${guard(
+ [this.formState.primarySeedUrl, this.formState.urlList],
+ priorityOrigins,
+ )}
+ @on-change=${(e: SelectBrowserProfileChangeEvent) => {
+ const profile = e.detail.value;
+
this.updateFormState({
- browserProfile: e.detail.value ?? null,
- })}
+ browserProfile: profile ?? null,
+ proxyId: profile?.proxyId ?? null,
+ });
+ }}
>
`)}
${this.renderHelpTextCol(infoTextFor["browserProfile"])}
@@ -2002,12 +2048,24 @@ https://archiveweb.page/images/${"logo.svg"}`}
proxies.default_proxy_id ?? undefined,
)}
.proxyServers=${proxies.servers}
- .proxyId="${this.formState.proxyId || ""}"
+ .proxyId=${profileProxyId || this.formState.proxyId || ""}
+ .profileProxyId=${profileProxyId}
@btrix-change=${(e: SelectCrawlerProxyChangeEvent) =>
this.updateFormState({
proxyId: e.detail.value,
})}
- >
+ >
+ ${when(
+ profileProxyId,
+ () => html`
+ ${msg("Set by profile")}
+ `,
+ )}
+
`),
this.renderHelpTextCol(infoTextFor["proxyId"]),
]
@@ -3253,7 +3311,7 @@ https://archiveweb.page/images/${"logo.svg"}`}
},
crawlerChannel:
this.formState.crawlerChannel || CrawlerChannelImage.Default,
- proxyId: this.formState.proxyId,
+ proxyId: this.formState.browserProfile?.proxyId || this.formState.proxyId,
};
return config;
@@ -3410,4 +3468,13 @@ https://archiveweb.page/images/${"logo.svg"}`}
console.debug(e);
}
}
+
+ private async getProfile(profileId: string, signal: AbortSignal) {
+ const data = await this.api.fetch(
+ `/orgs/${this.orgId}/profiles/${profileId}`,
+ { signal },
+ );
+
+ return data;
+ }
}
diff --git a/frontend/src/features/crawls/crawler-channel-badge.ts b/frontend/src/features/crawls/crawler-channel-badge.ts
new file mode 100644
index 0000000000..adce184bdc
--- /dev/null
+++ b/frontend/src/features/crawls/crawler-channel-badge.ts
@@ -0,0 +1,46 @@
+import { consume } from "@lit/context";
+import { localized } from "@lit/localize";
+import { html } from "lit";
+import { customElement, property } from "lit/decorators.js";
+import { ifDefined } from "lit/directives/if-defined.js";
+
+import { TailwindElement } from "@/classes/TailwindElement";
+import {
+ orgCrawlerChannelsContext,
+ type OrgCrawlerChannelsContext,
+} from "@/context/org-crawler-channels";
+import { CrawlerChannelImage } from "@/types/crawler";
+
+@customElement("btrix-crawler-channel-badge")
+@localized()
+export class CrawlerChannelBadge extends TailwindElement {
+ @consume({ context: orgCrawlerChannelsContext, subscribe: true })
+ private readonly crawlerChannels?: OrgCrawlerChannelsContext;
+
+ @property({ type: String })
+ channelId?: CrawlerChannelImage | AnyString;
+
+ render() {
+ if (!this.channelId || !this.crawlerChannels) return;
+
+ const crawlerChannel = this.crawlerChannels.find(
+ ({ id }) => id === this.channelId,
+ );
+
+ return html`
+
+
+ ${this.channelId}
+
+ `;
+ }
+}
diff --git a/frontend/src/features/crawls/index.ts b/frontend/src/features/crawls/index.ts
index 5254170741..c19583ee20 100644
--- a/frontend/src/features/crawls/index.ts
+++ b/frontend/src/features/crawls/index.ts
@@ -1,2 +1,4 @@
import("./crawl-list");
import("./crawl-state-filter");
+import("./crawler-channel-badge");
+import("./proxy-badge");
diff --git a/frontend/src/features/crawls/proxy-badge.ts b/frontend/src/features/crawls/proxy-badge.ts
new file mode 100644
index 0000000000..dabf5082a8
--- /dev/null
+++ b/frontend/src/features/crawls/proxy-badge.ts
@@ -0,0 +1,38 @@
+import { consume } from "@lit/context";
+import { localized } from "@lit/localize";
+import { html } from "lit";
+import { customElement, property } from "lit/decorators.js";
+import { ifDefined } from "lit/directives/if-defined.js";
+
+import { TailwindElement } from "@/classes/TailwindElement";
+import {
+ orgProxiesContext,
+ type OrgProxiesContext,
+} from "@/context/org-proxies";
+
+@customElement("btrix-proxy-badge")
+@localized()
+export class ProxyBadge extends TailwindElement {
+ @consume({ context: orgProxiesContext, subscribe: true })
+ private readonly orgProxies?: OrgProxiesContext;
+
+ @property({ type: String })
+ proxyId?: string;
+
+ render() {
+ if (!this.proxyId || !this.orgProxies) return;
+
+ const proxy = this.orgProxies.servers.find(({ id }) => id === this.proxyId);
+
+ return html`
+
+
+ ${proxy?.label || this.proxyId}
+
+ `;
+ }
+}
diff --git a/frontend/src/pages/org/browser-profiles-list.ts b/frontend/src/pages/org/browser-profiles-list.ts
index 394b7a2cbf..4ace718c76 100644
--- a/frontend/src/pages/org/browser-profiles-list.ts
+++ b/frontend/src/pages/org/browser-profiles-list.ts
@@ -1,6 +1,6 @@
import { localized, msg } from "@lit/localize";
import { Task } from "@lit/task";
-import { html, nothing, type PropertyValues } from "lit";
+import { html, type PropertyValues } from "lit";
import { customElement, state } from "lit/decorators.js";
import { when } from "lit/directives/when.js";
import queryString from "query-string";
@@ -18,6 +18,7 @@ import { parsePage, type PageChangeEvent } from "@/components/ui/pagination";
import type { BtrixChangeTagFilterEvent } from "@/components/ui/tag-filter/types";
import { ClipboardController } from "@/controllers/clipboard";
import { SearchParamsValue } from "@/controllers/searchParamsValue";
+import { originsWithRemainder } from "@/features/browser-profiles/templates/origins-with-remainder";
import { emptyMessage } from "@/layouts/emptyMessage";
import { page } from "@/layouts/page";
import { OrgTab } from "@/routes";
@@ -71,7 +72,7 @@ const columnsCss = [
"min-content", // Status
"[clickable-start] minmax(min-content, 1fr)", // Name
"30ch", // Tags
- "minmax(max-content, 1fr)", // Origins
+ "40ch", // Origins
"minmax(min-content, 20ch)", // Last modified
"[clickable-end] min-content", // Actions
].join(" ");
@@ -496,7 +497,7 @@ export class BrowserProfilesList extends BtrixElement {
${msg("Name")}
${msg("Tags")}
- ${msg("Configured Sites")}
+ ${msg("Saved Sites")}
${msg("Last Modified")}
@@ -520,8 +521,6 @@ export class BrowserProfilesList extends BtrixElement {
(a, b) => (b && a && b > a ? b : a),
data.created,
) || data.created;
- const startingUrl = data.origins[0];
- const otherOrigins = data.origins.slice(1);
return html`
-
-
- ${otherOrigins.length
- ? html`
- +${this.localize.number(otherOrigins.length)}
-
- ${otherOrigins.map((url) => html`- ${url}
`)}
-
- `
- : nothing}
+
+ ${originsWithRemainder(data.origins)}
- ${this.localize.relativeDate(modifiedByAnyDate)}
+ ${this.localize.relativeDate(modifiedByAnyDate, { capitalize: true })}
${this.renderActions(data)}
diff --git a/frontend/src/pages/org/browser-profiles/profile.ts b/frontend/src/pages/org/browser-profiles/profile.ts
index dc33472519..434e2e0c12 100644
--- a/frontend/src/pages/org/browser-profiles/profile.ts
+++ b/frontend/src/pages/org/browser-profiles/profile.ts
@@ -309,7 +309,7 @@ export class BrowserProfilesProfilePage extends BtrixElement {
const archivingDisabled = isArchivingDisabled(this.org);
return panel({
- heading: msg("Configured Sites"),
+ heading: msg("Saved Sites"),
actions: this.appState.isCrawler
? html`
@@ -467,7 +468,9 @@ export class BrowserProfilesProfilePage extends BtrixElement {
${this.renderDetail((profile) =>
- this.localize.relativeDate(modifiedByAnyDate || profile.created),
+ this.localize.relativeDate(modifiedByAnyDate || profile.created, {
+ capitalize: true,
+ }),
)}
diff --git a/frontend/src/pages/org/index.ts b/frontend/src/pages/org/index.ts
index 261855e125..b3530c830f 100644
--- a/frontend/src/pages/org/index.ts
+++ b/frontend/src/pages/org/index.ts
@@ -493,7 +493,7 @@ export class Org extends BtrixElement {
.proxyServers=${proxies.servers}
.crawlerChannels=${crawlerChannels}
defaultProxyId=${ifDefined(
- org.crawlingDefaults?.profileid ||
+ org.crawlingDefaults?.proxyId ||
proxies.default_proxy_id ||
undefined,
)}
diff --git a/frontend/src/pages/org/settings/components/crawling-defaults.ts b/frontend/src/pages/org/settings/components/crawling-defaults.ts
index 38284065d1..14ce869f75 100644
--- a/frontend/src/pages/org/settings/components/crawling-defaults.ts
+++ b/frontend/src/pages/org/settings/components/crawling-defaults.ts
@@ -20,6 +20,7 @@ import {
orgProxiesContext,
type OrgProxiesContext,
} from "@/context/org-proxies";
+import type { SelectBrowserProfile } from "@/features/browser-profiles/select-browser-profile";
import type { CustomBehaviorsTable } from "@/features/crawl-workflows/custom-behaviors-table";
import type { QueueExclusionTable } from "@/features/crawl-workflows/queue-exclusion-table";
import { columns, type Cols } from "@/layouts/columns";
@@ -79,6 +80,9 @@ export class OrgSettingsCrawlWorkflows extends BtrixElement {
@query("btrix-language-select")
languageSelect?: LanguageSelect | null;
+ @query("btrix-select-browser-profile")
+ browserProfileSelect?: SelectBrowserProfile | null;
+
@query("btrix-select-crawler-proxy")
proxySelect?: SelectCrawlerProxy | null;
@@ -362,7 +366,7 @@ export class OrgSettingsCrawlWorkflows extends BtrixElement {
behaviorTimeout: parseNumber(values.behaviorTimeoutSeconds),
pageExtraDelay: parseNumber(values.pageExtraDelaySeconds),
blockAds: values.blockAds === "on",
- profileid: values.profileid,
+ profileid: this.browserProfileSelect?.value || undefined,
crawlerChannel: values.crawlerChannel,
proxyId: this.proxySelect?.value || undefined,
userAgent: values.userAgent,
diff --git a/frontend/src/strings/crawl-workflows/infoText.ts b/frontend/src/strings/crawl-workflows/infoText.ts
index 042aa12c84..5a04269bb6 100644
--- a/frontend/src/strings/crawl-workflows/infoText.ts
+++ b/frontend/src/strings/crawl-workflows/infoText.ts
@@ -38,7 +38,7 @@ export const infoTextFor = {
msg(`Choose a custom profile to make use of saved cookies and logged-in
accounts. Note that websites may log profiles out after a period of time.`),
crawlerChannel: msg(
- `Choose a Browsertrix Crawler Release Channel. If available, other versions may provide new/experimental crawling features.`,
+ `Choose a Browsertrix Crawler release channel. If available, other versions may provide new or experimental crawling features.`,
),
blockAds: msg(
html`Blocks advertising content from being loaded. Uses
diff --git a/frontend/src/theme.stylesheet.css b/frontend/src/theme.stylesheet.css
index 59842237dd..191cd90e5a 100644
--- a/frontend/src/theme.stylesheet.css
+++ b/frontend/src/theme.stylesheet.css
@@ -240,7 +240,7 @@
sl-option:not([aria-selected="true"]):not(:disabled),
sl-menu-item:not([disabled]),
btrix-menu-item-link {
- @apply part-[base]:text-neutral-700 part-[base]:hover:bg-cyan-50/50 part-[base]:hover:text-cyan-700 part-[base]:focus-visible:bg-cyan-50/50;
+ @apply part-[base]:bg-white part-[base]:text-neutral-700 part-[base]:hover:bg-cyan-50/50 part-[base]:hover:text-cyan-700 part-[base]:focus:text-cyan-700 part-[base]:focus-visible:bg-cyan-50/50;
}
sl-option[aria-selected="true"] {
@@ -411,12 +411,14 @@
.font-monostyle {
@apply font-mono;
font-variation-settings: var(--font-monostyle-variation);
+ font-size: 95%;
}
/* Actually monospaced font */
.font-monospace {
@apply font-mono;
font-variation-settings: var(--font-monospace-variation);
+ font-size: 95%;
}
.truncate {
@@ -528,6 +530,11 @@
top: auto;
}
+html {
+ /* Fixes sl-input components resizing when sl-scroll-lock is removed */
+ scrollbar-gutter: stable;
+}
+
/* Ensure buttons in shadow dom inherit hover color */
[class^="hover\:text-"]::part(base):hover,
[class*=" hover\:text-"]::part(base):hover {
diff --git a/frontend/src/types/crawler.ts b/frontend/src/types/crawler.ts
index b68c0535d1..1b7eedfad3 100644
--- a/frontend/src/types/crawler.ts
+++ b/frontend/src/types/crawler.ts
@@ -66,6 +66,7 @@ export type WorkflowParams = {
schedule: string;
browserWindows: number;
profileid: string | null;
+ profileName?: string | null;
config: SeedConfig;
tags: string[];
crawlTimeout: number | null;
diff --git a/frontend/src/utils/workflow.ts b/frontend/src/utils/workflow.ts
index 14621dbbef..40f3449716 100644
--- a/frontend/src/utils/workflow.ts
+++ b/frontend/src/utils/workflow.ts
@@ -383,7 +383,10 @@ export function getInitialFormState(params: {
jobName: params.initialWorkflow.name || defaultFormState.jobName,
description: params.initialWorkflow.description,
browserProfile: params.initialWorkflow.profileid
- ? ({ id: params.initialWorkflow.profileid } as Profile)
+ ? ({
+ id: params.initialWorkflow.profileid,
+ name: params.initialWorkflow.profileName,
+ } as Profile)
: defaultFormState.browserProfile,
scopeType: primarySeedConfig.scopeType as FormState["scopeType"],
exclusions: seedsConfig.exclude?.length === 0 ? [""] : seedsConfig.exclude,