Enable duplicating and editing browser profile (#237)
* ensure editing other config options does not lose profile * support adding/editing/removing profile of existing config * when duplicating config, ensure profile setting is also copied in the duplicate
This commit is contained in:
parent
0c1dc2a1d1
commit
502d687620
@ -36,6 +36,9 @@ import("./not-found").then(({ NotFound }) => {
|
||||
import("./screencast").then(({ Screencast: Screencast }) => {
|
||||
customElements.define("btrix-screencast", Screencast);
|
||||
});
|
||||
import("./select-browser-profile").then(({ SelectBrowserProfile }) => {
|
||||
customElements.define("btrix-select-browser-profile", SelectBrowserProfile);
|
||||
});
|
||||
|
||||
customElements.define("btrix-alert", Alert);
|
||||
customElements.define("btrix-input", Input);
|
||||
|
199
frontend/src/components/select-browser-profile.ts
Normal file
199
frontend/src/components/select-browser-profile.ts
Normal file
@ -0,0 +1,199 @@
|
||||
import { html } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { msg, localized } from "@lit/localize";
|
||||
import orderBy from "lodash/fp/orderBy";
|
||||
|
||||
import type { AuthState } from "../utils/AuthService";
|
||||
import LiteElement from "../utils/LiteElement";
|
||||
import type { Profile } from "../pages/archive/types";
|
||||
|
||||
/**
|
||||
* Browser profile select dropdown
|
||||
*
|
||||
* Usage example:
|
||||
* ```ts
|
||||
* <btrix-select-browser-profile
|
||||
* authState=${authState}
|
||||
* archiveId=${archiveId}
|
||||
* on-change=${({value}) => selectedProfile = value}
|
||||
* ></btrix-select-browser-profile>
|
||||
* ```
|
||||
*
|
||||
* @event on-change
|
||||
*/
|
||||
@localized()
|
||||
export class SelectBrowserProfile extends LiteElement {
|
||||
@property({ type: Object })
|
||||
authState!: AuthState;
|
||||
|
||||
@property({ type: String })
|
||||
archiveId!: string;
|
||||
|
||||
@property({ type: String })
|
||||
profileId?: string;
|
||||
|
||||
@state()
|
||||
private selectedProfile?: Profile;
|
||||
|
||||
@state()
|
||||
private browserProfiles?: Profile[];
|
||||
|
||||
protected firstUpdated() {
|
||||
this.fetchBrowserProfiles();
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<sl-select
|
||||
name="browserProfile"
|
||||
label=${msg("Browser Profile")}
|
||||
clearable
|
||||
value=${this.selectedProfile?.id || ""}
|
||||
placeholder=${this.browserProfiles
|
||||
? msg("Select Profile")
|
||||
: msg("Loading")}
|
||||
?disabled=${!this.browserProfiles?.length}
|
||||
hoist
|
||||
@sl-change=${this.onChange}
|
||||
@sl-focus=${() => {
|
||||
// Refetch to keep list up to date
|
||||
this.fetchBrowserProfiles();
|
||||
}}
|
||||
@sl-hide=${this.stopProp}
|
||||
@sl-after-hide=${this.stopProp}
|
||||
>
|
||||
${this.browserProfiles
|
||||
? ""
|
||||
: html` <sl-spinner slot="prefix"></sl-spinner> `}
|
||||
${this.browserProfiles?.map(
|
||||
(profile) => html`
|
||||
<sl-menu-item value=${profile.id}>
|
||||
${profile.name}
|
||||
<div slot="suffix">
|
||||
<div class="text-xs">
|
||||
<sl-format-date
|
||||
date=${`${profile.created}Z` /** Z for UTC */}
|
||||
month="2-digit"
|
||||
day="2-digit"
|
||||
year="2-digit"
|
||||
></sl-format-date>
|
||||
</div></div
|
||||
></sl-menu-item>
|
||||
`
|
||||
)}
|
||||
</sl-select>
|
||||
|
||||
${this.browserProfiles && !this.browserProfiles.length
|
||||
? this.renderNoProfiles()
|
||||
: this.renderSelectedProfileInfo()}
|
||||
`;
|
||||
}
|
||||
|
||||
private renderSelectedProfileInfo() {
|
||||
if (!this.selectedProfile) return;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="mt-2 border bg-neutral-50 rounded p-2 text-sm flex justify-between"
|
||||
>
|
||||
${this.selectedProfile.description
|
||||
? html`<em class="text-slate-500"
|
||||
>${this.selectedProfile.description}</em
|
||||
>`
|
||||
: ""}
|
||||
<a
|
||||
href=${`/archives/${this.archiveId}/browser-profiles/profile/${this.selectedProfile.id}`}
|
||||
class="font-medium text-primary hover:text-indigo-500"
|
||||
target="_blank"
|
||||
>
|
||||
<span class="inline-block align-middle mr-1"
|
||||
>${msg("View profile")}</span
|
||||
>
|
||||
<sl-icon
|
||||
class="inline-block align-middle"
|
||||
name="box-arrow-up-right"
|
||||
></sl-icon>
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderNoProfiles() {
|
||||
return html`
|
||||
<div class="mt-2 text-sm text-neutral-500">
|
||||
<span class="inline-block align-middle"
|
||||
>${msg("No browser profiles found.")}</span
|
||||
>
|
||||
<a
|
||||
href=${`/archives/${this.archiveId}/browser-profiles/new`}
|
||||
class="font-medium text-primary hover:text-indigo-500"
|
||||
target="_blank"
|
||||
><span class="inline-block align-middle"
|
||||
>${msg("Create a browser profile")}</span
|
||||
>
|
||||
<sl-icon
|
||||
class="inline-block align-middle"
|
||||
name="box-arrow-up-right"
|
||||
></sl-icon
|
||||
></a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private onChange(e: any) {
|
||||
this.selectedProfile = this.browserProfiles?.find(
|
||||
({ id }) => id === e.target.value
|
||||
);
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("on-change", {
|
||||
detail: {
|
||||
value: this.selectedProfile,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch browser profiles and update internal state
|
||||
*/
|
||||
private async fetchBrowserProfiles(): Promise<void> {
|
||||
try {
|
||||
const data = await this.getProfiles();
|
||||
|
||||
this.browserProfiles = orderBy(["name", "created"])(["asc", "desc"])(
|
||||
data
|
||||
) as Profile[];
|
||||
|
||||
if (this.profileId && !this.selectedProfile) {
|
||||
this.selectedProfile = this.browserProfiles.find(
|
||||
({ id }) => id === this.profileId
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
this.notify({
|
||||
message: msg("Sorry, couldn't retrieve browser profiles at this time."),
|
||||
type: "danger",
|
||||
icon: "exclamation-octagon",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async getProfiles(): Promise<Profile[]> {
|
||||
const data = await this.apiFetch(
|
||||
`/archives/${this.archiveId}/profiles`,
|
||||
this.authState!
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop propgation of sl-select events.
|
||||
* Prevents bug where sl-dialog closes when dropdown closes
|
||||
* https://github.com/shoelace-style/shoelace/issues/170
|
||||
*/
|
||||
private stopProp(e: CustomEvent) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import { parse as yamlToJson, stringify as jsonToYaml } from "yaml";
|
||||
|
||||
import type { AuthState } from "../../utils/AuthService";
|
||||
import LiteElement, { html } from "../../utils/LiteElement";
|
||||
import type { InitialCrawlTemplate } from "./crawl-templates-new";
|
||||
import type { CrawlTemplate, CrawlConfig } from "./types";
|
||||
import { getUTCSchedule } from "./utils";
|
||||
import "../../components/crawl-scheduler";
|
||||
@ -670,6 +671,14 @@ export class CrawlTemplatesDetail extends LiteElement {
|
||||
${this.renderSeedsForm()}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<btrix-select-browser-profile
|
||||
archiveId=${this.archiveId}
|
||||
.profileId=${this.crawlTemplate.profileid || null}
|
||||
.authState=${this.authState}
|
||||
></btrix-select-browser-profile>
|
||||
</div>
|
||||
|
||||
<div class="text-right">
|
||||
<sl-button
|
||||
type="text"
|
||||
@ -1024,15 +1033,14 @@ export class CrawlTemplatesDetail extends LiteElement {
|
||||
private async duplicateConfig() {
|
||||
if (!this.crawlTemplate) return;
|
||||
|
||||
const config: CrawlTemplate["config"] = {
|
||||
...this.crawlTemplate.config,
|
||||
const crawlTemplate: InitialCrawlTemplate = {
|
||||
name: msg(str`${this.crawlTemplate.name} Copy`),
|
||||
config: this.crawlTemplate.config,
|
||||
profileid: this.crawlTemplate.profileid || null,
|
||||
};
|
||||
|
||||
this.navTo(`/archives/${this.archiveId}/crawl-templates/new`, {
|
||||
crawlTemplate: {
|
||||
name: msg(str`${this.crawlTemplate.name} Copy`),
|
||||
config,
|
||||
},
|
||||
crawlTemplate,
|
||||
});
|
||||
|
||||
this.notify({
|
||||
@ -1064,6 +1072,7 @@ export class CrawlTemplatesDetail extends LiteElement {
|
||||
detail: { formData: FormData };
|
||||
}) {
|
||||
const { formData } = e.detail;
|
||||
const profileId = (formData.get("browserProfile") as string) || null;
|
||||
|
||||
let config: CrawlConfig;
|
||||
|
||||
@ -1083,8 +1092,11 @@ export class CrawlTemplatesDetail extends LiteElement {
|
||||
};
|
||||
}
|
||||
|
||||
if (config) {
|
||||
await this.createRevisedTemplate(config);
|
||||
if (config || profileId) {
|
||||
await this.createRevisedTemplate({
|
||||
config,
|
||||
profileId,
|
||||
});
|
||||
}
|
||||
|
||||
this.openDialogName = undefined;
|
||||
@ -1221,15 +1233,20 @@ export class CrawlTemplatesDetail extends LiteElement {
|
||||
* Create new crawl template with revised crawl configuration
|
||||
* @param config Crawl config object
|
||||
*/
|
||||
private async createRevisedTemplate(config: CrawlConfig) {
|
||||
private async createRevisedTemplate({
|
||||
config,
|
||||
profileId,
|
||||
}: {
|
||||
config?: CrawlConfig;
|
||||
profileId: CrawlTemplate["profileid"];
|
||||
}) {
|
||||
this.isSubmittingUpdate = true;
|
||||
|
||||
const params = {
|
||||
oldId: this.crawlTemplate!.id,
|
||||
name: this.crawlTemplate!.name,
|
||||
schedule: this.crawlTemplate!.schedule,
|
||||
// runNow: this.crawlTemplate!.runNow,
|
||||
// crawlTimeout: this.crawlTemplate!.crawlTimeout,
|
||||
profileid: profileId,
|
||||
config,
|
||||
};
|
||||
|
||||
@ -1243,8 +1260,6 @@ export class CrawlTemplatesDetail extends LiteElement {
|
||||
}
|
||||
);
|
||||
|
||||
console.log(data);
|
||||
|
||||
this.navTo(
|
||||
`/archives/${this.archiveId}/crawl-templates/config/${data.added}`
|
||||
);
|
||||
@ -1272,8 +1287,6 @@ export class CrawlTemplatesDetail extends LiteElement {
|
||||
* @param params Crawl template properties to update
|
||||
*/
|
||||
private async updateTemplate(params: Partial<CrawlTemplate>): Promise<void> {
|
||||
console.log(params);
|
||||
|
||||
this.isSubmittingUpdate = true;
|
||||
|
||||
try {
|
||||
|
@ -11,6 +11,7 @@ import Fuse from "fuse.js";
|
||||
|
||||
import type { AuthState } from "../../utils/AuthService";
|
||||
import LiteElement, { html } from "../../utils/LiteElement";
|
||||
import type { InitialCrawlTemplate } from "./crawl-templates-new";
|
||||
import type { CrawlTemplate } from "./types";
|
||||
import { getUTCSchedule } from "./utils";
|
||||
import "../../components/crawl-scheduler";
|
||||
@ -581,15 +582,14 @@ export class CrawlTemplatesList extends LiteElement {
|
||||
* Create a new template using existing template data
|
||||
*/
|
||||
private async duplicateConfig(template: CrawlTemplate) {
|
||||
const config: CrawlTemplate["config"] = {
|
||||
...template.config,
|
||||
const crawlTemplate: InitialCrawlTemplate = {
|
||||
name: msg(str`${template.name} Copy`),
|
||||
config: template.config,
|
||||
profileid: template.profileid || null,
|
||||
};
|
||||
|
||||
this.navTo(`/archives/${this.archiveId}/crawl-templates/new`, {
|
||||
crawlTemplate: {
|
||||
name: msg(str`${template.name} Copy`),
|
||||
config,
|
||||
},
|
||||
crawlTemplate,
|
||||
});
|
||||
|
||||
this.notify({
|
||||
|
@ -3,7 +3,6 @@ import { ifDefined } from "lit/directives/if-defined.js";
|
||||
import { msg, localized, str } from "@lit/localize";
|
||||
import cronParser from "cron-parser";
|
||||
import { parse as yamlToJson, stringify as jsonToYaml } from "yaml";
|
||||
import orderBy from "lodash/fp/orderBy";
|
||||
|
||||
import type { AuthState } from "../../utils/AuthService";
|
||||
import LiteElement, { html } from "../../utils/LiteElement";
|
||||
@ -11,7 +10,7 @@ import { getLocaleTimeZone } from "../../utils/localization";
|
||||
import type { CrawlConfig, Profile } from "./types";
|
||||
import { getUTCSchedule } from "./utils";
|
||||
|
||||
export type NewCrawlTemplate = {
|
||||
type NewCrawlTemplate = {
|
||||
id?: string;
|
||||
name: string;
|
||||
schedule: string;
|
||||
@ -19,9 +18,14 @@ export type NewCrawlTemplate = {
|
||||
crawlTimeout?: number;
|
||||
scale: number;
|
||||
config: CrawlConfig;
|
||||
profileid: string;
|
||||
profileid: string | null;
|
||||
};
|
||||
|
||||
export type InitialCrawlTemplate = Pick<
|
||||
NewCrawlTemplate,
|
||||
"name" | "config" | "profileid"
|
||||
>;
|
||||
|
||||
const initialValues = {
|
||||
name: "",
|
||||
runNow: true,
|
||||
@ -55,10 +59,7 @@ export class CrawlTemplatesNew extends LiteElement {
|
||||
archiveId!: string;
|
||||
|
||||
@property({ type: Object })
|
||||
initialCrawlTemplate?: {
|
||||
name: string;
|
||||
config: CrawlConfig;
|
||||
};
|
||||
initialCrawlTemplate?: InitialCrawlTemplate;
|
||||
|
||||
@state()
|
||||
private isRunNow: boolean = initialValues.runNow;
|
||||
@ -85,15 +86,12 @@ export class CrawlTemplatesNew extends LiteElement {
|
||||
@state()
|
||||
private isSubmitting: boolean = false;
|
||||
|
||||
@state()
|
||||
private browserProfileId?: string | null;
|
||||
|
||||
@state()
|
||||
private serverError?: string;
|
||||
|
||||
@state()
|
||||
browserProfiles?: Profile[];
|
||||
|
||||
@state()
|
||||
selectedProfile?: Profile;
|
||||
|
||||
private get timeZone() {
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
}
|
||||
@ -137,19 +135,17 @@ export class CrawlTemplatesNew extends LiteElement {
|
||||
}
|
||||
this.initialCrawlTemplate = {
|
||||
name: this.initialCrawlTemplate?.name || initialValues.name,
|
||||
profileid: this.initialCrawlTemplate?.profileid || null,
|
||||
config: {
|
||||
...initialValues.config,
|
||||
...this.initialCrawlTemplate?.config,
|
||||
},
|
||||
};
|
||||
this.configCode = jsonToYaml(this.initialCrawlTemplate.config);
|
||||
this.browserProfileId = this.initialCrawlTemplate.profileid;
|
||||
super.connectedCallback();
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
this.fetchBrowserProfiles();
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<nav class="mb-5">
|
||||
@ -247,88 +243,15 @@ export class CrawlTemplatesNew extends LiteElement {
|
||||
></sl-input>
|
||||
|
||||
<div>
|
||||
<sl-select
|
||||
label=${msg("Browser Profile")}
|
||||
clearable
|
||||
value=${this.selectedProfile?.id || ""}
|
||||
?disabled=${!this.browserProfiles?.length}
|
||||
@sl-change=${(e: any) =>
|
||||
(this.selectedProfile = this.browserProfiles?.find(
|
||||
({ id }) => id === e.target.value
|
||||
))}
|
||||
@sl-focus=${() => {
|
||||
// Refetch to keep list up to date
|
||||
this.fetchBrowserProfiles();
|
||||
}}
|
||||
>
|
||||
${this.browserProfiles
|
||||
? ""
|
||||
: html` <sl-spinner slot="prefix"></sl-spinner> `}
|
||||
${this.browserProfiles?.map(
|
||||
(profile) => html`
|
||||
<sl-menu-item value=${profile.id}>
|
||||
${profile.name}
|
||||
<div slot="suffix">
|
||||
<div class="text-xs">
|
||||
<sl-format-date
|
||||
date=${`${profile.created}Z` /** Z for UTC */}
|
||||
month="2-digit"
|
||||
day="2-digit"
|
||||
year="2-digit"
|
||||
></sl-format-date>
|
||||
</div></div
|
||||
></sl-menu-item>
|
||||
`
|
||||
)}
|
||||
</sl-select>
|
||||
|
||||
${this.browserProfiles && !this.browserProfiles.length
|
||||
? html`
|
||||
<div class="mt-2 text-sm text-neutral-500">
|
||||
<span class="inline-block align-middle"
|
||||
>${msg("No browser profiles found.")}</span
|
||||
>
|
||||
<a
|
||||
href=${`/archives/${this.archiveId}/browser-profiles/new`}
|
||||
class="font-medium text-primary hover:text-indigo-500"
|
||||
target="_blank"
|
||||
><span class="inline-block align-middle"
|
||||
>${msg("Create a browser profile")}</span
|
||||
>
|
||||
<sl-icon
|
||||
class="inline-block align-middle"
|
||||
name="box-arrow-up-right"
|
||||
></sl-icon
|
||||
></a>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${this.selectedProfile
|
||||
? html`
|
||||
<div
|
||||
class="mt-2 border bg-slate-50 border-slate-100 rounded p-2 text-sm flex justify-between"
|
||||
>
|
||||
${this.selectedProfile.description
|
||||
? html`<em class="text-slate-500"
|
||||
>${this.selectedProfile.description}</em
|
||||
>`
|
||||
: ""}
|
||||
<a
|
||||
href=${`/archives/${this.archiveId}/browser-profiles/profile/${this.selectedProfile.id}`}
|
||||
class="font-medium text-primary hover:text-indigo-500"
|
||||
target="_blank"
|
||||
>
|
||||
<span class="inline-block align-middle mr-1"
|
||||
>${msg("View profile")}</span
|
||||
>
|
||||
<sl-icon
|
||||
class="inline-block align-middle"
|
||||
name="box-arrow-up-right"
|
||||
></sl-icon>
|
||||
</a>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<btrix-select-browser-profile
|
||||
archiveId=${this.archiveId}
|
||||
.profileId=${this.initialCrawlTemplate?.profileid || null}
|
||||
.authState=${this.authState}
|
||||
@on-change=${(e: any) =>
|
||||
(this.browserProfileId = e.detail.value
|
||||
? e.detail.value.id
|
||||
: null)}
|
||||
></btrix-select-browser-profile>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
@ -581,7 +504,7 @@ export class CrawlTemplatesNew extends LiteElement {
|
||||
runNow: this.isRunNow,
|
||||
crawlTimeout: crawlTimeoutMinutes ? +crawlTimeoutMinutes * 60 : 0,
|
||||
scale: +scale,
|
||||
profileid: this.selectedProfile?.id,
|
||||
profileid: this.browserProfileId,
|
||||
};
|
||||
|
||||
if (this.isConfigCodeView) {
|
||||
@ -661,34 +584,6 @@ export class CrawlTemplatesNew extends LiteElement {
|
||||
period,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch browser profiles and update internal state
|
||||
*/
|
||||
private async fetchBrowserProfiles(): Promise<void> {
|
||||
try {
|
||||
const data = await this.getProfiles();
|
||||
|
||||
this.browserProfiles = orderBy(["name", "created"])(["asc", "desc"])(
|
||||
data
|
||||
) as Profile[];
|
||||
} catch (e) {
|
||||
this.notify({
|
||||
message: msg("Sorry, couldn't retrieve browser profiles at this time."),
|
||||
type: "danger",
|
||||
icon: "exclamation-octagon",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async getProfiles(): Promise<Profile[]> {
|
||||
const data = await this.apiFetch(
|
||||
`/archives/${this.archiveId}/profiles`,
|
||||
this.authState!
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("btrix-crawl-templates-new", CrawlTemplatesNew);
|
||||
|
Loading…
Reference in New Issue
Block a user