diff --git a/frontend/src/pages/org/browser-profiles-detail.ts b/frontend/src/pages/org/browser-profiles-detail.ts index 1f51fb61..3e76139a 100644 --- a/frontend/src/pages/org/browser-profiles-detail.ts +++ b/frontend/src/pages/org/browser-profiles-detail.ts @@ -3,6 +3,7 @@ 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"; import queryString from "query-string"; import type { Profile } from "./types"; @@ -15,7 +16,8 @@ import { NotifyController } from "@/controllers/notify"; import type { BrowserConnectionChange } from "@/features/browser-profiles/profile-browser"; import { isApiError } from "@/utils/api"; import type { AuthState } from "@/utils/AuthService"; -import { getLocale } from "@/utils/localization"; +import { maxLengthValidator } from "@/utils/form"; +import { formatNumber, getLocale } from "@/utils/localization"; const DESCRIPTION_MAXLENGTH = 500; @@ -78,6 +80,8 @@ export class BrowserProfilesDetail extends TailwindElement { private readonly navigate = new NavigateController(this); private readonly notify = new NotifyController(this); + private readonly validateDescriptionMax = maxLengthValidator(500); + disconnectedCallback() { if (this.browserId) { void this.deleteBrowser(this.browserId); @@ -90,6 +94,9 @@ export class BrowserProfilesDetail extends TailwindElement { } render() { + const isBackedUp = + this.profile?.resource?.replicas && + this.profile.resource.replicas.length > 0; const none = html`${msg("None")}`; return html`
@@ -123,75 +130,69 @@ export class BrowserProfilesDetail extends TailwindElement {
-
-
-
-
${msg("Description")}
-
- ${this.profile - ? this.profile.description - ? this.profile.description.slice(0, DESCRIPTION_MAXLENGTH) - : none - : ""} -
-
-
-
- ${msg("Created at")} -
-
- ${this.profile - ? html` - - ` - : ""} -
-
-
-
- ${msg("Crawl Workflows")} - - - -
-
-
    - ${this.profile?.crawlconfigs?.map( - ({ id, name }) => html` -
  • - - ${name} - -
  • - `, - )} -
-
-
-
+
+ + +
+ ${isBackedUp + ? html` + ${msg("Backed Up")}` + : html` + ${msg("Not Backed Up")}`} +
+
+ + ${this.profile + ? this.profile.crawlerChannel + ? capitalize(this.profile.crawlerChannel) + : none + : nothing} + + + ${this.profile + ? html` + + ` + : nothing} + + + ${this.profile + ? html` ` + : nothing} + +
-
+

${msg("Browser Profile")} @@ -245,6 +246,46 @@ export class BrowserProfilesDetail extends TailwindElement { )}

+
+
+

+ ${msg("Description")} +

+ ${when( + this.isCrawler, + () => html` + (this.isEditDialogOpen = true)} + label=${msg("Edit description")} + > + `, + )} +
+
+ ${this.profile + ? this.profile.description || + html` +
+ ${msg("No description added.")} +
+ ` + : nothing} +
+
+ +
+

+ ${msg("Crawl Workflows")}${this.profile?.crawlconfigs?.length + ? html` + (${formatNumber(this.profile.crawlconfigs.length)}) + ` + : nothing} +

+ ${this.renderCrawlWorkflows()} +
+ ${msg( "Are you sure you want to discard changes to this browser profile?", @@ -280,6 +321,33 @@ export class BrowserProfilesDetail extends TailwindElement { `; } + private renderCrawlWorkflows() { + if (this.profile?.crawlconfigs?.length) { + return html``; + } + + return html`
+ ${msg("Not used in any crawl workflows.")} +
`; + } + private readonly renderVisitedSites = () => { return html`
@@ -356,6 +424,8 @@ export class BrowserProfilesDetail extends TailwindElement { private renderEditProfile() { if (!this.profile) return; + const { helpText, validate } = this.validateDescriptionMax; + return html`
@@ -372,9 +442,12 @@ export class BrowserProfilesDetail extends TailwindElement {
@@ -628,9 +701,10 @@ export class BrowserProfilesDetail extends TailwindElement { private async onSubmitEdit(e: SubmitEvent) { e.preventDefault(); - this.isSubmittingProfileChange = true; + const formEl = e.target as HTMLFormElement; + if (!(await this.checkFormValidity(formEl))) return; - const formData = new FormData(e.target as HTMLFormElement); + const formData = new FormData(formEl); const name = formData.get("name") as string; const description = formData.get("description") as string; @@ -639,6 +713,8 @@ export class BrowserProfilesDetail extends TailwindElement { description, }; + this.isSubmittingProfileChange = true; + try { const data = await this.api.fetch<{ updated: boolean }>( `/orgs/${this.orgId}/profiles/${this.profileId}`, @@ -687,12 +763,8 @@ export class BrowserProfilesDetail extends TailwindElement { this.isSubmittingProfileChange = false; } - /** - * Stop propgation of sl-select events. - * Prevents bug where sl-dialog closes when dropdown closes - * https://github.com/shoelace-style/shoelace/issues/170 - */ - private stopProp(e: CustomEvent) { - e.stopPropagation(); + async checkFormValidity(formEl: HTMLFormElement) { + await this.updateComplete; + return !formEl.querySelector("[data-invalid]"); } } diff --git a/frontend/src/pages/org/browser-profiles-list.ts b/frontend/src/pages/org/browser-profiles-list.ts index 58e15815..5ee3d33b 100644 --- a/frontend/src/pages/org/browser-profiles-list.ts +++ b/frontend/src/pages/org/browser-profiles-list.ts @@ -1,5 +1,5 @@ -import { localized, msg } from "@lit/localize"; -import { nothing } from "lit"; +import { localized, msg, str } from "@lit/localize"; +import { nothing, type PropertyValues } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { when } from "lit/directives/when.js"; import queryString from "query-string"; @@ -8,12 +8,15 @@ import type { Profile } from "./types"; import type { SelectNewDialogEvent } from "."; -import type { APIPaginatedList } from "@/types/api"; +import type { PageChangeEvent } from "@/components/ui/pagination"; +import type { APIPaginatedList, APIPaginationQuery } from "@/types/api"; import type { Browser } from "@/types/browser"; import type { AuthState } from "@/utils/AuthService"; import LiteElement, { html } from "@/utils/LiteElement"; import { getLocale } from "@/utils/localization"; +const INITIAL_PAGE_SIZE = 20; + /** * Usage: * ```ts @@ -32,31 +35,45 @@ export class BrowserProfilesList extends LiteElement { @property({ type: String }) orgId!: string; - @state() - browserProfiles?: Profile[]; + @property({ type: Boolean }) + isCrawler = false; - firstUpdated() { - void this.fetchBrowserProfiles(); + @state() + browserProfiles?: APIPaginatedList; + + protected willUpdate( + changedProperties: PropertyValues & Map, + ) { + if (changedProperties.has("orgId")) { + void this.fetchBrowserProfiles(); + } } render() { return html`
-
-

${msg("Browser Profiles")}

- { - this.dispatchEvent( - new CustomEvent("select-new-dialog", { - detail: "browser-profile", - }) as SelectNewDialogEvent, - ); - }} - > - - ${msg("New Browser Profile")} - +
+

+ ${msg("Browser Profiles")} +

+ ${when( + this.isCrawler, + () => html` + { + this.dispatchEvent( + new CustomEvent("select-new-dialog", { + detail: "browser-profile", + }) as SelectNewDialogEvent, + ); + }} + > + + ${msg("New Browser Profile")} + + `, + )}
${this.renderTable()}
`; @@ -65,17 +82,14 @@ export class BrowserProfilesList extends LiteElement { private renderTable() { return html` - - ${msg("Backed up status")} - - + ${msg("Name")} - ${msg("Date Created")} + ${msg("Last Updated")} ${msg("Visited URLs")} @@ -84,21 +98,34 @@ export class BrowserProfilesList extends LiteElement { ${msg("Row Actions")} - ${this.browserProfiles?.length - ? html` - - ${this.browserProfiles.map(this.renderItem)} - - ` - : nothing} + ${when(this.browserProfiles, ({ total, items }) => + total + ? html` + + ${items.map(this.renderItem)} + + ` + : nothing, + )} ${when( this.browserProfiles, - (browserProfiles) => - browserProfiles.length - ? nothing + ({ total, page, pageSize }) => + total + ? html` +
+ { + void this.fetchBrowserProfiles({ page: e.detail.page }); + }} + > +
+ ` : html`

@@ -117,24 +144,12 @@ export class BrowserProfilesList extends LiteElement {

`; private readonly renderItem = (data: Profile) => { - const isBackedUp = - data.resource?.replicas && data.resource.replicas.length > 0; return html` - - - - - ${data.name} - ${data.description - ? html`
-
${data.description}
-
` - : nothing}
- ${data.origins.join(", ")} + + ${data.origins[0]}${data.origins.length > 1 + ? html` + + ${msg(str`+${data.origins.length - 1}`)} + + ` + : nothing} + ${this.renderActions(data)} @@ -256,9 +282,7 @@ export class BrowserProfilesList extends LiteElement { icon: "check2-circle", }); - this.browserProfiles = this.browserProfiles!.filter( - (p) => p.id !== profile.id, - ); + void this.fetchBrowserProfiles(); } } catch (e) { this.notify({ @@ -287,9 +311,17 @@ export class BrowserProfilesList extends LiteElement { /** * Fetch browser profiles and update internal state */ - private async fetchBrowserProfiles(): Promise { + private async fetchBrowserProfiles( + params?: APIPaginationQuery, + ): Promise { try { - const data = await this.getProfiles(); + const data = await this.getProfiles({ + page: params?.page || this.browserProfiles?.page || 1, + pageSize: + params?.pageSize || + this.browserProfiles?.pageSize || + INITIAL_PAGE_SIZE, + }); this.browserProfiles = data; } catch (e) { @@ -301,12 +333,16 @@ export class BrowserProfilesList extends LiteElement { } } - private async getProfiles() { + private async getProfiles(params: APIPaginationQuery) { + const query = queryString.stringify(params, { + arrayFormat: "comma", + }); + const data = await this.apiFetch>( - `/orgs/${this.orgId}/profiles`, + `/orgs/${this.orgId}/profiles?${query}`, this.authState!, ); - return data.items; + return data; } } diff --git a/frontend/src/pages/org/index.ts b/frontend/src/pages/org/index.ts index ea1cf937..d7ba3583 100644 --- a/frontend/src/pages/org/index.ts +++ b/frontend/src/pages/org/index.ts @@ -659,6 +659,7 @@ export class Org extends LiteElement { return html``; } diff --git a/frontend/src/types/crawler.ts b/frontend/src/types/crawler.ts index 7f33009d..be965409 100644 --- a/frontend/src/types/crawler.ts +++ b/frontend/src/types/crawler.ts @@ -98,6 +98,9 @@ export type Profile = { name: string; description: string; created: string; + createdByName: string | null; + modified: string | null; + modifiedByName: string | null; origins: string[]; profileId: string; baseProfileName: string;