Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 67 additions & 4 deletions frontend/src/features/browser-profiles/start-browser-dialog.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { consume } from "@lit/context";
import { localized, msg } from "@lit/localize";
import { Task } from "@lit/task";
import type {
SlButton,
SlChangeEvent,
Expand All @@ -11,6 +12,7 @@ import { html, nothing, type PropertyValues } 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 { Details } from "@/components/ui/details";
Expand All @@ -26,6 +28,7 @@ import {
type OrgProxiesContext,
} from "@/context/org-proxies";
import type { Profile } from "@/types/crawler";
import type { WorkflowSearchValues } from "@/types/workflow";

type StartBrowserEventDetail = {
url?: string;
Expand Down Expand Up @@ -89,6 +92,48 @@ export class StartBrowserDialog extends BtrixElement {
@query("#submit-button")
private readonly submitButton?: SlButton | null;

// Get unique origins from workflow first seeds/crawl start URLs
private readonly workflowOrigins = new Task(this, {
task: async ([profile], { signal }) => {
if (!profile) return null;

const query = queryString.stringify({ profileIds: profile.id });

try {
const { firstSeeds } = await this.api.fetch<WorkflowSearchValues>(
`/orgs/${this.orgId}/crawlconfigs/search-values?${query}`,
{ signal },
);

const profileUrls = profile.origins.map((origin) => new URL(origin));
const originMap: { [url: string]: boolean } = {};

firstSeeds.forEach((seed) => {
const seedUrl = new URL(seed);

// Only check domain names without www
if (
profileUrls.some((url) => {
return (
seedUrl.hostname.replace(/^www\./, "") ===
url.hostname.replace(/^www\./, "")
);
})
) {
return;
}

originMap[seedUrl.origin] = true;
});

return Object.keys(originMap);
} catch (e) {
console.debug(e);
}
},
args: () => [this.profile] as const,
});

protected willUpdate(changedProperties: PropertyValues): void {
if (changedProperties.has("initialUrl")) {
this.loadUrl = this.initialUrl;
Expand Down Expand Up @@ -238,6 +283,15 @@ export class StartBrowserDialog extends BtrixElement {
}

private readonly renderUrl = (profile: Profile) => {
const option = (url: string) =>
html`<sl-option
class="part-[label]:overflow-hidden"
value=${url}
title=${url}
>
<div class="truncate">${url}</div>
</sl-option>`;

return html`<sl-select
name=${PRIMARY_SITE_FIELD_NAME}
label=${msg("Primary Site")}
Expand All @@ -253,12 +307,21 @@ export class StartBrowserDialog extends BtrixElement {
}
}}
>
<sl-option value="">${msg("New Site")}</sl-option>
<sl-divider></sl-divider>
<sl-menu-label>${msg("Saved Sites")}</sl-menu-label>
${profile.origins.map(
(url) => html` <sl-option value=${url}>${url}</sl-option> `,
${profile.origins.map(option)}
${when(this.workflowOrigins.value, (seeds) =>
seeds.length
? html`
<sl-divider></sl-divider>
<sl-menu-label
>${msg("Suggestions from Related Workflows")}</sl-menu-label
>
${seeds.map(option)}
`
: nothing,
)}
<sl-divider></sl-divider>
<sl-option value="">${msg("New Site")}</sl-option>
</sl-select>

<div class="mt-4">
Expand Down
100 changes: 69 additions & 31 deletions frontend/src/features/crawl-workflows/workflow-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { ShareableNotice } from "./templates/shareable-notice";

import { BtrixElement } from "@/classes/BtrixElement";
import type { OverflowDropdown } from "@/components/ui/overflow-dropdown";
import { WorkflowTab } from "@/routes";
import { OrgTab, WorkflowTab } from "@/routes";
import { noData } from "@/strings/ui";
import type { ListWorkflow } from "@/types/crawler";
import { humanizeSchedule } from "@/utils/cron";
Expand All @@ -40,15 +40,15 @@ export type WorkflowColumnName =
| "actions";

const columnWidths = {
name: "minmax(18rem, 1fr)",
// TODO Consolidate with table.stylesheet.css
// https://github.com/webrecorder/browsertrix/issues/3001
name: "[clickable-start] minmax(18rem, 1fr)",
"latest-crawl": "minmax(15rem, 18rem)",
"total-crawls": "minmax(6rem, 9rem)",
modified: "minmax(12rem, 15rem)",
actions: "3rem",
actions: "[clickable-end] 3rem",
} as const satisfies Record<WorkflowColumnName, string>;

// postcss-lit-disable-next-line
const mediumBreakpointCss = css`30rem`;
// postcss-lit-disable-next-line
const largeBreakpointCss = css`60rem`;
// postcss-lit-disable-next-line
Expand All @@ -65,11 +65,6 @@ const rowCss = css`
right: 0;
}

@media only screen and (min-width: ${mediumBreakpointCss}) {
.row {
grid-template-columns: repeat(2, 1fr);
}
}
@media only screen and (min-width: ${largeBreakpointCss}) {
.row {
grid-template-columns: var(--btrix-workflow-list-columns);
Expand Down Expand Up @@ -252,6 +247,43 @@ export class WorkflowListItem extends BtrixElement {
border-left: 1px solid var(--sl-panel-border-color);
}
}

/*
* TODO Consolidate with table.stylesheet.css
* https://github.com/webrecorder/browsertrix/issues/3001
*/
.rowClickTarget--cell {
display: grid;
grid-template-columns: subgrid;
white-space: nowrap;
overflow: hidden;
}

.rowClickTarget {
max-width: 100%;
}

.col sl-tooltip > *,
.col btrix-popover > *,
.col btrix-overflow-dropdown {
/* Place above .rowClickTarget::after overlay */
z-index: 10;
position: relative;
}

.rowClickTarget::after {
content: "";
display: block;
position: absolute;
inset: 0;
grid-column: clickable-start / clickable-end;
}

.rowClickTarget:focus-visible {
outline: var(--sl-focus-ring);
outline-offset: -0.25rem;
border-radius: 0.5rem;
}
`,
];

Expand All @@ -271,13 +303,18 @@ export class WorkflowListItem extends BtrixElement {
@query("btrix-overflow-dropdown")
dropdownMenu!: OverflowDropdown;

@query("a")
private readonly anchor?: HTMLAnchorElement | null;

private readonly columnTemplate = {
name: () =>
html`<div class="col">
<div class="detail url items-center truncate">
name: () => {
const href = `/orgs/${this.orgSlugState}/${OrgTab.Workflows}/${this.workflow?.id}/${this.workflow?.lastCrawlState?.startsWith("failed") ? WorkflowTab.Logs : WorkflowTab.LatestCrawl}`;

return html`<div class="col rowClickTarget--cell">
<a class="detail url rowClickTarget items-center truncate" href=${href}>
${when(this.workflow?.shareable, ShareableNotice)}
${this.safeRender(this.renderName)}
</div>
</a>
<div class="desc truncate">
${this.safeRender((workflow) => {
if (workflow.schedule) {
Expand All @@ -291,7 +328,8 @@ export class WorkflowListItem extends BtrixElement {
return msg("---");
})}
</div>
</div>`,
</div>`;
},
"latest-crawl": () =>
html`<div class="col">${this.safeRender(this.renderLatestCrawl)}</div>`,
"total-crawls": () =>
Expand Down Expand Up @@ -358,20 +396,7 @@ export class WorkflowListItem extends BtrixElement {
} satisfies Record<WorkflowColumnName, () => TemplateResult>;

render() {
return html`<div
class="item row"
role="button"
@click=${async (e: MouseEvent) => {
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);
}}
>
return html`<div class="item row">
${this.columns
? this.columns.map((col) => this.columnTemplate[col]())
: Object.values(this.columnTemplate).map((render) => render())}
Expand Down Expand Up @@ -480,7 +505,7 @@ export class WorkflowListItem extends BtrixElement {

return html`
<sl-tooltip hoist placement="bottom" ?disabled=${!tooltipContent}>
<div>
<div class="w-max" @click=${this.redirectEventToAnchor}>
<div class="detail">${status}</div>
<div class="desc duration">${duration}</div>
</div>
Expand All @@ -495,7 +520,7 @@ export class WorkflowListItem extends BtrixElement {

return html`
<sl-tooltip hoist placement="bottom">
<div>
<div class="w-max" @click=${this.redirectEventToAnchor}>
<div class="detail truncate">
${workflow.modifiedByName
? html`<btrix-user-chip
Expand Down Expand Up @@ -548,6 +573,19 @@ export class WorkflowListItem extends BtrixElement {
>${nameSuffix}
`;
};

/*
* TODO Remove when refactored to `btrix-table`
* https://github.com/webrecorder/browsertrix/issues/3001
*/
private readonly redirectEventToAnchor = (e: MouseEvent) => {
if (!e.defaultPrevented) {
const newEvent = new MouseEvent(e.type, e);
e.stopPropagation();

this.anchor?.dispatchEvent(newEvent);
}
};
}

@customElement("btrix-workflow-list")
Expand Down
22 changes: 11 additions & 11 deletions frontend/src/pages/org/browser-profiles/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,17 @@ export class BrowserProfilesProfilePage extends BtrixElement {
(workflow) =>
html`<btrix-workflow-list-item .workflow=${workflow}>
<sl-menu slot="menu">
<sl-menu-item
@click=${() =>
void this.openBrowser({ url: workflow.firstSeed })}
>
<sl-icon
slot="prefix"
name="window-fullscreen"
></sl-icon>
${msg("Load Crawl Start URL")}
</sl-menu-item>
<sl-divider></sl-divider>
${when(
this.appState.isCrawler,
() => html`
Expand All @@ -679,19 +690,8 @@ export class BrowserProfilesProfilePage extends BtrixElement {
<sl-icon name="gear" slot="prefix"></sl-icon>
${msg("Edit Workflow Settings")}
</btrix-menu-item-link>
<sl-divider></sl-divider>
`,
)}
<btrix-menu-item-link
href="${this.navigate
.orgBasePath}/${OrgTab.Workflows}/${workflow.id}"
>
<sl-icon
name="arrow-return-right"
slot="prefix"
></sl-icon>
${msg("Go to Workflow")}
</btrix-menu-item-link>
</sl-menu>
</btrix-workflow-list-item>`,
)}
Expand Down
9 changes: 2 additions & 7 deletions frontend/src/pages/org/workflows-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import { WorkflowTab } from "@/routes";
import { deleteConfirmation } from "@/strings/ui";
import type { APIPaginatedList, APIPaginationQuery } from "@/types/api";
import { type CrawlState } from "@/types/crawlState";
import { type StorageSeedFile } from "@/types/workflow";
import type { StorageSeedFile, WorkflowSearchValues } from "@/types/workflow";
import { isApiError } from "@/utils/api";
import { settingsForDuplicate } from "@/utils/crawl-workflows/settingsForDuplicate";
import { renderName } from "@/utils/crawler";
Expand Down Expand Up @@ -994,12 +994,7 @@ export class WorkflowsList extends BtrixElement {

private async fetchConfigSearchValues() {
try {
const data: {
crawlIds: string[];
names: string[];
descriptions: string[];
firstSeeds: string[];
} = await this.api.fetch(
const data = await this.api.fetch<WorkflowSearchValues>(
`/orgs/${this.orgId}/crawlconfigs/search-values`,
);

Expand Down
7 changes: 6 additions & 1 deletion frontend/src/theme.stylesheet.css
Original file line number Diff line number Diff line change
Expand Up @@ -237,11 +237,16 @@
}

/* Adjust menu item hover and focus styles */
sl-menu-item,
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;
}

sl-option[aria-selected="true"] {
@apply part-[base]:bg-cyan-50 part-[base]:text-cyan-700;
}

/* Add menu item variants */
.menu-item-success {
@apply part-[base]:text-success part-[base]:hover:bg-success-50 part-[base]:hover:text-success-700 part-[base]:focus-visible:bg-success-50;
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/types/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,10 @@ export type StorageSeedFile = StorageFile & {
firstSeed: string;
seedCount: number;
};

export type WorkflowSearchValues = {
crawlIds: string[];
names: string[];
descriptions: string[];
firstSeeds: string[];
};
Loading