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:
sua yoo 2022-06-04 08:26:19 -07:00 committed by GitHub
parent 0c1dc2a1d1
commit 502d687620
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 259 additions and 149 deletions

View File

@ -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);

View 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();
}
}

View File

@ -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 {

View File

@ -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({

View File

@ -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);