Separate "run now" switch from scheduling options (#1175)

This commit is contained in:
sua yoo 2023-09-21 19:18:57 -07:00 committed by GitHub
parent 83f80d4103
commit d05a27e8a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 232 additions and 158 deletions

View File

@ -1449,7 +1449,6 @@ export class WorkflowDetail extends LiteElement {
message: msg("Starting crawl."), message: msg("Starting crawl."),
variant: "success", variant: "success",
icon: "check2-circle", icon: "check2-circle",
duration: 8000,
}); });
} catch (e: any) { } catch (e: any) {
let message = msg("Sorry, couldn't run crawl at this time."); let message = msg("Sorry, couldn't run crawl at this time.");

View File

@ -1,11 +1,13 @@
import type { LitElement, TemplateResult } from "lit"; import type { LitElement, TemplateResult } from "lit";
import { html as staticHtml, unsafeStatic } from "lit/static-html.js"; import { html as staticHtml, unsafeStatic } from "lit/static-html.js";
import type { import type {
SlChangeEvent,
SlCheckbox, SlCheckbox,
SlInput, SlInput,
SlRadio, SlRadio,
SlRadioGroup, SlRadioGroup,
SlSelect, SlSelect,
SlSwitch,
SlTextarea, SlTextarea,
} from "@shoelace-style/shoelace"; } from "@shoelace-style/shoelace";
import { state, property, query, queryAsync } from "lit/decorators.js"; import { state, property, query, queryAsync } from "lit/decorators.js";
@ -90,7 +92,7 @@ type FormState = {
scale: WorkflowParams["scale"]; scale: WorkflowParams["scale"];
blockAds: WorkflowParams["config"]["blockAds"]; blockAds: WorkflowParams["config"]["blockAds"];
lang: WorkflowParams["config"]["lang"]; lang: WorkflowParams["config"]["lang"];
scheduleType: "now" | "date" | "cron" | "none"; scheduleType: "date" | "cron" | "none";
scheduleFrequency: "daily" | "weekly" | "monthly" | ""; scheduleFrequency: "daily" | "weekly" | "monthly" | "";
scheduleDayOfMonth?: number; scheduleDayOfMonth?: number;
scheduleDayOfWeek?: number; scheduleDayOfWeek?: number;
@ -165,7 +167,7 @@ const getDefaultFormState = (): FormState => ({
scale: 1, scale: 1,
blockAds: true, blockAds: true,
lang: undefined, lang: undefined,
scheduleType: "now", scheduleType: "none",
scheduleFrequency: "weekly", scheduleFrequency: "weekly",
scheduleDayOfMonth: new Date().getDate(), scheduleDayOfMonth: new Date().getDate(),
scheduleDayOfWeek: new Date().getDay(), scheduleDayOfWeek: new Date().getDay(),
@ -174,7 +176,7 @@ const getDefaultFormState = (): FormState => ({
minute: 0, minute: 0,
period: "AM", period: "AM",
}, },
runNow: false, runNow: true,
jobName: "", jobName: "",
browserProfile: null, browserProfile: null,
tags: [], tags: [],
@ -183,9 +185,6 @@ const getDefaultFormState = (): FormState => ({
autoscrollBehavior: true, autoscrollBehavior: true,
}); });
const defaultProgressState = getDefaultProgressState(); const defaultProgressState = getDefaultProgressState();
const orderedTabNames = STEPS.filter(
(stepName) => defaultProgressState.tabs[stepName as StepName]
) as StepName[];
function getLocalizedWeekDays() { function getLocalizedWeekDays() {
const now = new Date(); const now = new Date();
@ -300,7 +299,6 @@ export class CrawlConfigEditor extends LiteElement {
FormState["scheduleType"], FormState["scheduleType"],
string string
> = { > = {
now: msg("Run Immediately on Save"),
date: msg("Run on a Specific Date & Time"), date: msg("Run on a Specific Date & Time"),
cron: msg("Run on a Recurring Basis"), cron: msg("Run on a Recurring Basis"),
none: msg("No Schedule"), none: msg("No Schedule"),
@ -473,11 +471,7 @@ export class CrawlConfigEditor extends LiteElement {
period: hours > 11 ? "PM" : "AM", period: hours > 11 ? "PM" : "AM",
}; };
} else { } else {
if (this.configId) { formState.scheduleType = "none";
formState.scheduleType = "none";
} else {
formState.scheduleType = "now";
}
} }
if (this.initialWorkflow.tags?.length) { if (this.initialWorkflow.tags?.length) {
@ -554,6 +548,14 @@ export class CrawlConfigEditor extends LiteElement {
crawlMetadata: msg("Metadata"), crawlMetadata: msg("Metadata"),
confirmSettings: msg("Review Settings"), confirmSettings: msg("Review Settings"),
}; };
let orderedTabNames = STEPS.filter(
(stepName) => defaultProgressState.tabs[stepName as StepName]
) as StepName[];
if (this.configId) {
// Remove review tab
orderedTabNames = orderedTabNames.slice(0, -1);
}
return html` return html`
<form <form
@ -566,7 +568,11 @@ export class CrawlConfigEditor extends LiteElement {
> >
<btrix-tab-list <btrix-tab-list
activePanel="newJobConfig-${this.progressState.activeTab}" activePanel="newJobConfig-${this.progressState.activeTab}"
progressPanel="newJobConfig-${this.progressState.activeTab}" progressPanel=${ifDefined(
this.configId
? undefined
: `newJobConfig-${this.progressState.activeTab}`
)}
> >
<header slot="header" class="flex justify-between items-baseline"> <header slot="header" class="flex justify-between items-baseline">
<h3 class="font-semibold"> <h3 class="font-semibold">
@ -637,34 +643,31 @@ export class CrawlConfigEditor extends LiteElement {
const isActive = tabName === this.progressState.activeTab; const isActive = tabName === this.progressState.activeTab;
const isConfirmSettings = tabName === "confirmSettings"; const isConfirmSettings = tabName === "confirmSettings";
const { error: isInvalid, completed } = this.progressState.tabs[tabName]; const { error: isInvalid, completed } = this.progressState.tabs[tabName];
const iconProps = { let icon: TemplateResult = html``;
name: "circle",
library: "default",
class: "text-neutral-400",
};
if (isConfirmSettings) {
iconProps.name = "info-circle";
iconProps.class = "text-base";
} else {
if (isInvalid) {
iconProps.name = "exclamation-circle";
iconProps.class = "text-danger";
} else if (isActive) {
iconProps.name = "pencil-circle-dashed";
iconProps.library = "app";
iconProps.class = "text-base";
} else if (completed) {
iconProps.name = "check-circle";
}
}
return html` if (!this.configId) {
<btrix-tab const iconProps = {
slot="nav" name: "circle",
name="newJobConfig-${tabName}" library: "default",
class="whitespace-nowrap" class: "text-neutral-400",
@click=${this.tabClickHandler(tabName)} };
> if (isConfirmSettings) {
iconProps.name = "info-circle";
iconProps.class = "text-base";
} else {
if (isInvalid) {
iconProps.name = "exclamation-circle";
iconProps.class = "text-danger";
} else if (isActive) {
iconProps.name = "pencil-circle-dashed";
iconProps.library = "app";
iconProps.class = "text-base";
} else if (completed) {
iconProps.name = "check-circle";
}
}
icon = html`
<sl-tooltip <sl-tooltip
content=${msg("Form section contains errors")} content=${msg("Form section contains errors")}
?disabled=${!isInvalid} ?disabled=${!isInvalid}
@ -676,7 +679,22 @@ export class CrawlConfigEditor extends LiteElement {
class="inline-block align-middle mr-1 text-base ${iconProps.class}" class="inline-block align-middle mr-1 text-base ${iconProps.class}"
></sl-icon> ></sl-icon>
</sl-tooltip> </sl-tooltip>
<span class="inline-block align-middle whitespace-normal"> `;
}
return html`
<btrix-tab
slot="nav"
name="newJobConfig-${tabName}"
class="whitespace-nowrap"
@click=${this.tabClickHandler(tabName)}
>
${icon}
<span
class="inline-block align-middle whitespace-normal${this.configId
? " ml-1"
: ""}"
>
${content} ${content}
</span> </span>
</btrix-tab> </btrix-tab>
@ -688,8 +706,13 @@ export class CrawlConfigEditor extends LiteElement {
{ isFirst = false, isLast = false } = {} { isFirst = false, isLast = false } = {}
) { ) {
return html` return html`
<div class="border rounded-lg flex flex-col h-full"> <div class="flex flex-col h-full min-h-[21rem]">
<div class="flex-1 p-6 grid grid-cols-5 gap-4"> <div
class="flex-1 p-6 grid grid-cols-5 gap-4 border rounded-lg ${!this
.configId && !isLast
? "border-b-0 rounded-b-none"
: "mb-4"}"
>
${content} ${content}
${when(this.serverError, () => ${when(this.serverError, () =>
this.renderErrorAlert(this.serverError!) this.renderErrorAlert(this.serverError!)
@ -702,98 +725,144 @@ export class CrawlConfigEditor extends LiteElement {
} }
private renderFooter({ isFirst = false, isLast = false }) { private renderFooter({ isFirst = false, isLast = false }) {
if (this.configId) {
return html`
<footer
class="px-6 py-4 flex gap-2 items-center justify-end border rounded-lg"
>
<div class="mr-auto">${this.renderRunNowToggle()}</div>
<aside class="text-xs text-neutral-500">
${msg("Changes in all sections will be saved")}
</aside>
<sl-button
type="submit"
size="small"
variant="primary"
?disabled=${this.isSubmitting}
?loading=${this.isSubmitting}
>
${msg("Save Workflow")}
</sl-button>
</footer>
`;
}
if (!this.configId) {
return html`
<footer
class="px-6 py-4 flex gap-2 items-center justify-end border ${isLast
? "rounded-lg"
: "rounded-b-lg"}"
>
${this.renderSteppedFooterButtons({ isFirst, isLast })}
</footer>
`;
}
return html` return html`
<div class="px-6 py-4 border-t flex justify-between"> <div class="px-6 py-4 border-t flex gap-2 items-center justify-end">
${isFirst
? html`
<sl-button size="small" type="reset">
<sl-icon slot="prefix" name="chevron-left"></sl-icon>
${this.configId ? msg("Cancel") : msg("Start Over")}
</sl-button>
`
: html`
<sl-button size="small" @click=${this.backStep}>
<sl-icon slot="prefix" name="chevron-left"></sl-icon>
${msg("Previous Step")}
</sl-button>
`}
${when( ${when(
this.configId, this.configId,
() => html` () => html`
<div> <div class="mr-auto">${this.renderRunNowToggle()}</div>
${when( <sl-button
!isLast, type="submit"
() => html` size="small"
<sl-button class="mr-1" size="small" @click=${this.nextStep}> variant="primary"
<sl-icon slot="suffix" name="chevron-right"></sl-icon> ?disabled=${this.isSubmitting}
${msg("Next")} ?loading=${this.isSubmitting}
</sl-button> >
` ${msg("Save Changes")}
)} </sl-button>
<sl-button
type="submit"
size="small"
variant="primary"
?disabled=${this.isSubmitting}
?loading=${this.isSubmitting}
>
${msg("Save Changes")}
</sl-button>
</div>
`, `,
() => () => this.renderSteppedFooterButtons({ isFirst, isLast })
isLast
? html`<sl-button
type="submit"
size="small"
variant="primary"
?disabled=${this.isSubmitting || this.formHasError}
?loading=${this.isSubmitting}
>
${this.formState.scheduleType === "now" ||
this.formState.runNow
? msg("Save & Run Crawl")
: this.formState.scheduleType === "none"
? msg("Save Workflow")
: msg("Save & Schedule Crawl")}
</sl-button>`
: html`
<div>
<sl-button
class="mr-1"
size="small"
variant="primary"
@click=${this.nextStep}
>
<sl-icon slot="suffix" name="chevron-right"></sl-icon>
${msg("Next Step")}
</sl-button>
<sl-button
size="small"
@click=${() => {
if (this.hasRequiredFields()) {
this.updateProgressState({
activeTab: "confirmSettings",
});
} else {
this.nextStep();
}
}}
>
<sl-icon
slot="suffix"
name="chevron-double-right"
></sl-icon>
${msg("Review & Save")}
</sl-button>
</div>
`
)} )}
</div> </div>
`; `;
} }
private renderSteppedFooterButtons({
isFirst,
isLast,
}: {
isFirst: boolean;
isLast: boolean;
}) {
if (isLast) {
return html`<sl-button
class="mr-auto"
size="small"
@click=${this.backStep}
>
<sl-icon slot="prefix" name="chevron-left"></sl-icon>
${msg("Previous Step")}
</sl-button>
${this.renderRunNowToggle()}
<sl-button
type="submit"
size="small"
variant="primary"
?disabled=${this.isSubmitting || this.formHasError}
?loading=${this.isSubmitting}
>
${msg("Save Workflow")}
</sl-button>`;
}
return html`
${isFirst
? html`
<sl-button class="mr-auto" size="small" type="reset">
<sl-icon slot="prefix" name="chevron-left"></sl-icon>
${msg("Start Over")}
</sl-button>
`
: html`
<sl-button class="mr-auto" size="small" @click=${this.backStep}>
<sl-icon slot="prefix" name="chevron-left"></sl-icon>
${msg("Previous Step")}
</sl-button>
`}
<sl-button size="small" variant="primary" @click=${this.nextStep}>
<sl-icon slot="suffix" name="chevron-right"></sl-icon>
${msg("Next Step")}
</sl-button>
<sl-button
size="small"
@click=${() => {
if (this.hasRequiredFields()) {
this.updateProgressState({
activeTab: "confirmSettings",
});
} else {
this.nextStep();
}
}}
>
<sl-icon slot="suffix" name="chevron-double-right"></sl-icon>
${msg("Review & Save")}
</sl-button>
`;
}
private renderRunNowToggle() {
return html`
<sl-switch
class="mr-1"
?checked=${this.formState.runNow}
@sl-change=${(e: SlChangeEvent) => {
this.updateFormState(
{
runNow: (e.target as SlSwitch).checked,
},
true
);
}}
>
${msg("Run on Save")}
</sl-switch>
`;
}
private renderSectionHeading(content: TemplateResult | string) { private renderSectionHeading(content: TemplateResult | string) {
return html` return html`
<btrix-section-heading class="col-span-5"> <btrix-section-heading class="col-span-5">
@ -1505,24 +1574,23 @@ https://archiveweb.page/images/${"logo.svg"}`}
return html` return html`
${this.renderFormCol(html` ${this.renderFormCol(html`
<sl-radio-group <sl-radio-group
label=${msg("Crawl Schedule Type")} label=${msg("Crawl Schedule")}
name="scheduleType" name="scheduleType"
value=${this.formState.scheduleType} value=${this.formState.scheduleType}
@sl-change=${(e: Event) => @sl-change=${(e: Event) =>
this.updateFormState({ this.updateFormState({
scheduleType: (e.target as SlRadio) scheduleType: (e.target as SlRadio)
.value as FormState["scheduleType"], .value as FormState["scheduleType"],
runNow: (e.target as SlRadio).value === "now",
})} })}
> >
<sl-radio value="now">${this.scheduleTypeLabels["now"]}</sl-radio>
<sl-radio value="cron">${this.scheduleTypeLabels["cron"]}</sl-radio>
<sl-radio value="none">${this.scheduleTypeLabels["none"]}</sl-radio> <sl-radio value="none">${this.scheduleTypeLabels["none"]}</sl-radio>
<sl-radio value="cron">${this.scheduleTypeLabels["cron"]}</sl-radio>
</sl-radio-group> </sl-radio-group>
`)} `)}
${this.renderHelpTextCol( ${this.renderHelpTextCol(
msg(`Should a crawl run immediately when setup is complete, on a set msg(
day, or on a recurring schedule?`) `Configure crawls to run every day, week, or month at a specified time.`
)
)} )}
${when(this.formState.scheduleType === "cron", this.renderScheduleCron)} ${when(this.formState.scheduleType === "cron", this.renderScheduleCron)}
`; `;
@ -1641,17 +1709,6 @@ https://archiveweb.page/images/${"logo.svg"}`}
${this.renderHelpTextCol( ${this.renderHelpTextCol(
msg(`A crawl will run at this time in your current timezone.`) msg(`A crawl will run at this time in your current timezone.`)
)} )}
${this.renderFormCol(html`<sl-checkbox
name="runNow"
?checked=${this.formState.runNow}
>
${msg("Also run a crawl immediately on save")}
</sl-checkbox>`)}
${this.renderHelpTextCol(
msg(`If checked, a crawl will run at the time specified above and also
once when setup is complete.`),
false
)}
`; `;
}; };
@ -1908,6 +1965,9 @@ https://archiveweb.page/images/${"logo.svg"}`}
private updateFormStateOnChange(e: Event) { private updateFormStateOnChange(e: Event) {
const elem = e.target as SlTextarea | SlInput | SlCheckbox; const elem = e.target as SlTextarea | SlInput | SlCheckbox;
const name = elem.name; const name = elem.name;
if (!this.formState.hasOwnProperty(name)) {
return;
}
const tagName = elem.tagName.toLowerCase(); const tagName = elem.tagName.toLowerCase();
let value: any; let value: any;
switch (tagName) { switch (tagName) {
@ -1932,11 +1992,9 @@ https://archiveweb.page/images/${"logo.svg"}`}
default: default:
return; return;
} }
if (name in this.formState) { this.updateFormState({
this.updateFormState({ [name]: value,
[name]: value, });
});
}
} }
private tabClickHandler = (step: StepName) => (e: MouseEvent) => { private tabClickHandler = (step: StepName) => (e: MouseEvent) => {
@ -1967,6 +2025,11 @@ https://archiveweb.page/images/${"logo.svg"}`}
const nextTab = STEPS[STEPS.indexOf(activeTab!) + 1] as StepName; const nextTab = STEPS[STEPS.indexOf(activeTab!) + 1] as StepName;
this.updateProgressState({ this.updateProgressState({
activeTab: nextTab, activeTab: nextTab,
tabs: {
[activeTab]: {
completed: true,
},
},
}); });
} }
} }
@ -2048,13 +2111,13 @@ https://archiveweb.page/images/${"logo.svg"}`}
if (crawlId && storageQuotaReached) { if (crawlId && storageQuotaReached) {
this.notify({ this.notify({
title: msg("Workflow saved."), title: msg("Workflow saved without starting crawl."),
message: msg( message: msg(
"Could not start crawl with new workflow settings due to storage quota." "Could not run crawl with new workflow settings due to storage quota."
), ),
variant: "warning", variant: "warning",
icon: "exclamation-triangle", icon: "exclamation-circle",
duration: 8000, duration: 12000,
}); });
} else { } else {
let message = msg("Workflow created."); let message = msg("Workflow created.");
@ -2078,12 +2141,24 @@ https://archiveweb.page/images/${"logo.svg"}`}
); );
} catch (e: any) { } catch (e: any) {
if (e?.isApiError) { if (e?.isApiError) {
const isConfigError = ({ loc }: any) => if (e.details === "crawl_already_running") {
loc.some((v: string) => v === "config"); this.notify({
if (e.details && e.details.some(isConfigError)) { title: msg("Workflow saved without starting crawl."),
this.serverError = this.formatConfigServerError(e.details); message: msg(
"Could not run crawl with new workflow settings due to already running crawl."
),
variant: "warning",
icon: "exclamation-circle",
duration: 12000,
});
} else { } else {
this.serverError = e.message; const isConfigError = ({ loc }: any) =>
loc.some((v: string) => v === "config");
if (Array.isArray(e.details) && e.details.some(isConfigError)) {
this.serverError = this.formatConfigServerError(e.details);
} else {
this.serverError = e.message;
}
} }
} else { } else {
this.serverError = msg("Something unexpected went wrong"); this.serverError = msg("Something unexpected went wrong");
@ -2154,7 +2229,7 @@ https://archiveweb.page/images/${"logo.svg"}`}
description: this.formState.description, description: this.formState.description,
scale: this.formState.scale, scale: this.formState.scale,
profileid: this.formState.browserProfile?.id || "", profileid: this.formState.browserProfile?.id || "",
runNow: this.formState.runNow || this.formState.scheduleType === "now", runNow: this.formState.runNow,
schedule: this.formState.scheduleType === "cron" ? this.utcSchedule : "", schedule: this.formState.scheduleType === "cron" ? this.utcSchedule : "",
crawlTimeout: this.formState.crawlTimeoutMinutes * 60, crawlTimeout: this.formState.crawlTimeoutMinutes * 60,
maxCrawlSize: this.formState.maxCrawlSizeGB * BYTES_PER_GB, maxCrawlSize: this.formState.maxCrawlSizeGB * BYTES_PER_GB,