parent
							
								
									c2aa4e6319
								
							
						
					
					
						commit
						d144591dbf
					
				| @ -5,6 +5,7 @@ | |||||||
|   "license": "MIT", |   "license": "MIT", | ||||||
|   "private": true, |   "private": true, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|  |     "@cheap-glitch/mi-cron": "^1.0.1", | ||||||
|     "@formatjs/intl-displaynames": "^5.2.5", |     "@formatjs/intl-displaynames": "^5.2.5", | ||||||
|     "@formatjs/intl-getcanonicallocales": "^1.8.0", |     "@formatjs/intl-getcanonicallocales": "^1.8.0", | ||||||
|     "@lit/localize": "^0.11.1", |     "@lit/localize": "^0.11.1", | ||||||
| @ -12,8 +13,6 @@ | |||||||
|     "@xstate/fsm": "^1.6.2", |     "@xstate/fsm": "^1.6.2", | ||||||
|     "axios": "^0.22.0", |     "axios": "^0.22.0", | ||||||
|     "color": "^4.0.1", |     "color": "^4.0.1", | ||||||
|     "cron-parser": "^4.2.1", |  | ||||||
|     "cronstrue": "^1.123.0", |  | ||||||
|     "fuse.js": "^6.5.3", |     "fuse.js": "^6.5.3", | ||||||
|     "lit": "^2.0.0", |     "lit": "^2.0.0", | ||||||
|     "lodash": "^4.17.21", |     "lodash": "^4.17.21", | ||||||
|  | |||||||
| @ -1,14 +1,25 @@ | |||||||
| import { state, property } from "lit/decorators.js"; | import { state, property } from "lit/decorators.js"; | ||||||
| import { msg, localized, str } from "@lit/localize"; | import { msg, localized, str } from "@lit/localize"; | ||||||
| import cronstrue from "cronstrue"; // TODO localize
 | import { parseCron } from "@cheap-glitch/mi-cron"; | ||||||
| 
 | 
 | ||||||
| import LiteElement, { html } from "../utils/LiteElement"; | import LiteElement, { html } from "../utils/LiteElement"; | ||||||
| import { getLocaleTimeZone } from "../utils/localization"; | import { | ||||||
|  |   ScheduleInterval, | ||||||
|  |   getScheduleInterval, | ||||||
|  |   getUTCSchedule, | ||||||
|  |   humanizeSchedule, | ||||||
|  |   humanizeNextDate, | ||||||
|  | } from "../utils/cron"; | ||||||
| import type { CrawlTemplate } from "../pages/archive/types"; | import type { CrawlTemplate } from "../pages/archive/types"; | ||||||
| 
 | 
 | ||||||
| const nowHour = new Date().getHours(); | const hours = Array.from({ length: 12 }).map((x, i) => ({ | ||||||
| const initialHours = nowHour % 12 || 12; |   value: i + 1, | ||||||
| const initialPeriod = nowHour > 11 ? "PM" : "AM"; |   label: `${i + 1}`, | ||||||
|  | })); | ||||||
|  | const minutes = Array.from({ length: 60 }).map((x, i) => ({ | ||||||
|  |   value: i, | ||||||
|  |   label: `${i}`.padStart(2, "0"), | ||||||
|  | })); | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Usage: |  * Usage: | ||||||
| @ -36,49 +47,28 @@ export class CrawlTemplatesScheduler extends LiteElement { | |||||||
|   cancelable?: boolean = false; |   cancelable?: boolean = false; | ||||||
| 
 | 
 | ||||||
|   @state() |   @state() | ||||||
|   private editedSchedule?: string; |   private scheduleInterval: ScheduleInterval | "" = ""; | ||||||
| 
 | 
 | ||||||
|   @state() |   @state() | ||||||
|   private isScheduleDisabled?: boolean; |   private scheduleTime: { hour: number; minute: number; period: "AM" | "PM" } = | ||||||
|  |     { | ||||||
|  |       hour: new Date().getHours() % 12 || 12, | ||||||
|  |       minute: 0, | ||||||
|  |       period: new Date().getHours() > 11 ? "PM" : "AM", | ||||||
|  |     }; | ||||||
| 
 | 
 | ||||||
|   @state() |   private get isScheduleDisabled(): boolean { | ||||||
|   private schedulePeriod: "AM" | "PM" = initialPeriod; |     return !this.scheduleInterval; | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|   private get timeZoneShortName() { |   firstUpdated() { | ||||||
|     return getLocaleTimeZone(); |     this.setInitialValues(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   render() { |   render() { | ||||||
|     // TODO consolidate with new
 |     // TODO consolidate with new
 | ||||||
|     const hours = Array.from({ length: 12 }).map((x, i) => ({ |  | ||||||
|       value: i + 1, |  | ||||||
|       label: `${i + 1}`, |  | ||||||
|     })); |  | ||||||
|     const minutes = Array.from({ length: 60 }).map((x, i) => ({ |  | ||||||
|       value: i, |  | ||||||
|       label: `${i}`.padStart(2, "0"), |  | ||||||
|     })); |  | ||||||
| 
 | 
 | ||||||
|     const getInitialScheduleInterval = (schedule: string) => { |     const utcSchedule = this.getUTCSchedule(); | ||||||
|       const [minute, hour, dayofMonth, month, dayOfWeek] = schedule.split(" "); |  | ||||||
|       if (dayofMonth === "*") { |  | ||||||
|         if (dayOfWeek === "*") { |  | ||||||
|           return "daily"; |  | ||||||
|         } |  | ||||||
|         return "weekly"; |  | ||||||
|       } |  | ||||||
|       return "monthly"; |  | ||||||
|     }; |  | ||||||
|     const scheduleIntervalsMap = { |  | ||||||
|       daily: `0 ${nowHour} * * *`, |  | ||||||
|       weekly: `0 ${nowHour} * * ${new Date().getDay()}`, |  | ||||||
|       monthly: `0 ${nowHour} ${new Date().getDate()} * *`, |  | ||||||
|     }; |  | ||||||
|     const initialInterval = this.schedule |  | ||||||
|       ? getInitialScheduleInterval(this.schedule) |  | ||||||
|       : "weekly"; |  | ||||||
|     const nextSchedule = |  | ||||||
|       this.editedSchedule || scheduleIntervalsMap[initialInterval]; |  | ||||||
| 
 | 
 | ||||||
|     return html` |     return html` | ||||||
|       <sl-form @sl-submit=${this.onSubmit}> |       <sl-form @sl-submit=${this.onSubmit}> | ||||||
| @ -87,23 +77,12 @@ export class CrawlTemplatesScheduler extends LiteElement { | |||||||
|             <sl-select |             <sl-select | ||||||
|               name="scheduleInterval" |               name="scheduleInterval" | ||||||
|               label=${msg("Recurring crawls")} |               label=${msg("Recurring crawls")} | ||||||
|               value=${initialInterval} |               value=${this.scheduleInterval} | ||||||
|               hoist |               hoist | ||||||
|               @sl-hide=${this.stopProp} |               @sl-hide=${this.stopProp} | ||||||
|               @sl-after-hide=${this.stopProp} |               @sl-after-hide=${this.stopProp} | ||||||
|               @sl-select=${(e: any) => { |               @sl-select=${(e: any) => { | ||||||
|                 if (e.target.value) { |                 this.scheduleInterval = e.target.value; | ||||||
|                   this.isScheduleDisabled = false; |  | ||||||
|                   this.editedSchedule = `${nextSchedule |  | ||||||
|                     .split(" ") |  | ||||||
|                     .slice(0, 2) |  | ||||||
|                     .join(" ")} ${(scheduleIntervalsMap as any)[e.target.value] |  | ||||||
|                     .split(" ") |  | ||||||
|                     .slice(2) |  | ||||||
|                     .join(" ")}`;
 |  | ||||||
|                 } else { |  | ||||||
|                   this.isScheduleDisabled = true; |  | ||||||
|                 } |  | ||||||
|               }} |               }} | ||||||
|             > |             > | ||||||
|               <sl-menu-item value="">${msg("None")}</sl-menu-item> |               <sl-menu-item value="">${msg("None")}</sl-menu-item> | ||||||
| @ -120,20 +99,16 @@ export class CrawlTemplatesScheduler extends LiteElement { | |||||||
|               <sl-select |               <sl-select | ||||||
|                 class="grow" |                 class="grow" | ||||||
|                 name="scheduleHour" |                 name="scheduleHour" | ||||||
|                 value=${initialHours} |                 value=${this.scheduleTime.hour} | ||||||
|                 ?disabled=${this.isScheduleDisabled} |                 ?disabled=${this.isScheduleDisabled} | ||||||
|                 hoist |                 hoist | ||||||
|                 @sl-hide=${this.stopProp} |                 @sl-hide=${this.stopProp} | ||||||
|                 @sl-after-hide=${this.stopProp} |                 @sl-after-hide=${this.stopProp} | ||||||
|                 @sl-select=${(e: any) => { |                 @sl-select=${(e: any) => { | ||||||
|                   const hour = +e.target.value; |                   this.scheduleTime = { | ||||||
|                   const period = this.schedulePeriod; |                     ...this.scheduleTime, | ||||||
| 
 |                     hour: +e.target.value, | ||||||
|                   this.setScheduleHour({ |                   }; | ||||||
|                     hour, |  | ||||||
|                     period, |  | ||||||
|                     schedule: nextSchedule, |  | ||||||
|                   }); |  | ||||||
|                 }} |                 }} | ||||||
|               > |               > | ||||||
|                 ${hours.map( |                 ${hours.map( | ||||||
| @ -145,16 +120,16 @@ export class CrawlTemplatesScheduler extends LiteElement { | |||||||
|               <sl-select |               <sl-select | ||||||
|                 class="grow" |                 class="grow" | ||||||
|                 name="scheduleMinute" |                 name="scheduleMinute" | ||||||
|                 value="0" |                 value=${this.scheduleTime.minute} | ||||||
|                 ?disabled=${this.isScheduleDisabled} |                 ?disabled=${this.isScheduleDisabled} | ||||||
|                 hoist |                 hoist | ||||||
|                 @sl-hide=${this.stopProp} |                 @sl-hide=${this.stopProp} | ||||||
|                 @sl-after-hide=${this.stopProp} |                 @sl-after-hide=${this.stopProp} | ||||||
|                 @sl-select=${(e: any) => |                 @sl-select=${(e: any) => | ||||||
|                   (this.editedSchedule = `${e.target.value} ${nextSchedule |                   (this.scheduleTime = { | ||||||
|                     .split(" ") |                     ...this.scheduleTime, | ||||||
|                     .slice(1) |                     minute: +e.target.value, | ||||||
|                     .join(" ")}`)}
 |                   })} | ||||||
|               > |               > | ||||||
|                 ${minutes.map( |                 ${minutes.map( | ||||||
|                   ({ value, label }) => |                   ({ value, label }) => | ||||||
| @ -165,66 +140,52 @@ export class CrawlTemplatesScheduler extends LiteElement { | |||||||
|             <input |             <input | ||||||
|               name="schedulePeriod" |               name="schedulePeriod" | ||||||
|               type="hidden" |               type="hidden" | ||||||
|               value=${this.schedulePeriod} |               value=${this.scheduleTime.period} | ||||||
|             /> |             /> | ||||||
|             <sl-button-group> |             <sl-button-group> | ||||||
|               <sl-button |               <sl-button | ||||||
|                 type=${this.schedulePeriod === "AM" ? "neutral" : "default"} |                 type=${this.scheduleTime.period === "AM" | ||||||
|                 aria-selected=${this.schedulePeriod === "AM"} |                   ? "neutral" | ||||||
|  |                   : "default"} | ||||||
|  |                 aria-selected=${this.scheduleTime.period === "AM"} | ||||||
|                 ?disabled=${this.isScheduleDisabled} |                 ?disabled=${this.isScheduleDisabled} | ||||||
|                 @click=${(e: any) => { |                 @click=${() => | ||||||
|                   const hour = +e.target |                   (this.scheduleTime = { | ||||||
|                     .closest("sl-form") |                     ...this.scheduleTime, | ||||||
|                     .querySelector('sl-select[name="scheduleHour"]').value; |                     period: "AM", | ||||||
|                   const period = "AM"; |                   })} | ||||||
| 
 |  | ||||||
|                   this.schedulePeriod = period; |  | ||||||
|                   this.setScheduleHour({ |  | ||||||
|                     hour, |  | ||||||
|                     period, |  | ||||||
|                     schedule: nextSchedule, |  | ||||||
|                   }); |  | ||||||
|                 }} |  | ||||||
|                 >${msg("AM", { desc: "Time AM/PM" })}</sl-button |                 >${msg("AM", { desc: "Time AM/PM" })}</sl-button | ||||||
|               > |               > | ||||||
|               <sl-button |               <sl-button | ||||||
|                 type=${this.schedulePeriod === "PM" ? "neutral" : "default"} |                 type=${this.scheduleTime.period === "PM" | ||||||
|                 aria-selected=${this.schedulePeriod === "PM"} |                   ? "neutral" | ||||||
|  |                   : "default"} | ||||||
|  |                 aria-selected=${this.scheduleTime.period === "PM"} | ||||||
|                 ?disabled=${this.isScheduleDisabled} |                 ?disabled=${this.isScheduleDisabled} | ||||||
|                 @click=${(e: any) => { |                 @click=${() => | ||||||
|                   const hour = +e.target |                   (this.scheduleTime = { | ||||||
|                     .closest("sl-form") |                     ...this.scheduleTime, | ||||||
|                     .querySelector('sl-select[name="scheduleHour"]').value; |                     period: "PM", | ||||||
|                   const period = "PM"; |                   })} | ||||||
| 
 |  | ||||||
|                   this.schedulePeriod = period; |  | ||||||
|                   this.setScheduleHour({ |  | ||||||
|                     hour, |  | ||||||
|                     period, |  | ||||||
|                     schedule: nextSchedule, |  | ||||||
|                   }); |  | ||||||
|                 }} |  | ||||||
|                 >${msg("PM", { desc: "Time AM/PM" })}</sl-button |                 >${msg("PM", { desc: "Time AM/PM" })}</sl-button | ||||||
|               > |               > | ||||||
|             </sl-button-group> |             </sl-button-group> | ||||||
|           </div> |           </div> | ||||||
|         </fieldset> |         </fieldset> | ||||||
| 
 | 
 | ||||||
|         <div class="mt-5"> |         <div class="mt-5 bg-neutral-50 rounded p-3 text-sm text-neutral-800"> | ||||||
|           ${this.isScheduleDisabled |           ${this.isScheduleDisabled | ||||||
|             ? msg(html`<span class="font-medium"
 |             ? html`<span class="font-medium"
 | ||||||
|                 >Crawls will not repeat.</span |                 >${msg("Crawls will not repeat.")}</span | ||||||
|               >`)
 |               >` | ||||||
|             : msg( |             : html` | ||||||
|                 html`<span class="font-medium">New schedule will be:</span
 |                 <p>${msg(str`Schedule: ${humanizeSchedule(utcSchedule)}.`)}</p> | ||||||
|                   ><br /> |                 <p> | ||||||
|                   <span class="text-0-600" |                   ${msg( | ||||||
|                     >${cronstrue.toString(nextSchedule, { |                     str`Next scheduled run: ${humanizeNextDate(utcSchedule)}.` | ||||||
|                       verbose: true, |                   )} | ||||||
|                     })} |                 </p> | ||||||
|                     (in ${this.timeZoneShortName} time zone)</span |               `}
 | ||||||
|                   >` |  | ||||||
|               )} |  | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
|         <div class="mt-5${this.cancelable ? " text-right" : ""}"> |         <div class="mt-5${this.cancelable ? " text-right" : ""}"> | ||||||
| @ -256,34 +217,6 @@ export class CrawlTemplatesScheduler extends LiteElement { | |||||||
|     this.dispatchEvent(new CustomEvent("submit", event)); |     this.dispatchEvent(new CustomEvent("submit", event)); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** |  | ||||||
|    * Set correct local hour in schedule in 24-hr format |  | ||||||
|    **/ |  | ||||||
|   private setScheduleHour({ |  | ||||||
|     hour, |  | ||||||
|     period, |  | ||||||
|     schedule, |  | ||||||
|   }: { |  | ||||||
|     hour: number; |  | ||||||
|     period: "AM" | "PM"; |  | ||||||
|     schedule: string; |  | ||||||
|   }) { |  | ||||||
|     // Convert 12-hr to 24-hr time
 |  | ||||||
|     let periodOffset = 0; |  | ||||||
| 
 |  | ||||||
|     if (hour === 12) { |  | ||||||
|       if (period === "AM") { |  | ||||||
|         periodOffset = -12; |  | ||||||
|       } |  | ||||||
|     } else if (period === "PM") { |  | ||||||
|       periodOffset = 12; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     this.editedSchedule = `${schedule.split(" ")[0]} ${ |  | ||||||
|       hour + periodOffset |  | ||||||
|     } ${schedule.split(" ").slice(2).join(" ")}`;
 |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** |   /** | ||||||
|    * Stop propgation of sl-select events. |    * Stop propgation of sl-select events. | ||||||
|    * Prevents bug where sl-dialog closes when dropdown closes |    * Prevents bug where sl-dialog closes when dropdown closes | ||||||
| @ -292,6 +225,29 @@ export class CrawlTemplatesScheduler extends LiteElement { | |||||||
|   private stopProp(e: CustomEvent) { |   private stopProp(e: CustomEvent) { | ||||||
|     e.stopPropagation(); |     e.stopPropagation(); | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   private setInitialValues() { | ||||||
|  |     if (this.schedule) { | ||||||
|  |       const nextDate = parseCron.nextDate(this.schedule)!; | ||||||
|  |       const hours = nextDate.getHours(); | ||||||
|  | 
 | ||||||
|  |       this.scheduleTime = { | ||||||
|  |         hour: hours % 12, | ||||||
|  |         minute: nextDate.getMinutes(), | ||||||
|  |         period: hours > 11 ? "PM" : "AM", | ||||||
|  |       }; | ||||||
|  |       this.scheduleInterval = getScheduleInterval(this.schedule); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private getUTCSchedule(): string { | ||||||
|  |     if (!this.scheduleInterval) return ""; | ||||||
|  | 
 | ||||||
|  |     return getUTCSchedule({ | ||||||
|  |       interval: this.scheduleInterval, | ||||||
|  |       ...this.scheduleTime, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| customElements.define("btrix-crawl-scheduler", CrawlTemplatesScheduler); | customElements.define("btrix-crawl-scheduler", CrawlTemplatesScheduler); | ||||||
|  | |||||||
| @ -2,14 +2,13 @@ import type { HTMLTemplateResult } from "lit"; | |||||||
| import { state, property } from "lit/decorators.js"; | import { state, property } from "lit/decorators.js"; | ||||||
| import { ifDefined } from "lit/directives/if-defined.js"; | import { ifDefined } from "lit/directives/if-defined.js"; | ||||||
| import { msg, localized, str } from "@lit/localize"; | import { msg, localized, str } from "@lit/localize"; | ||||||
| import cronstrue from "cronstrue"; // TODO localize
 |  | ||||||
| import { parse as yamlToJson, stringify as jsonToYaml } from "yaml"; | import { parse as yamlToJson, stringify as jsonToYaml } from "yaml"; | ||||||
| 
 | 
 | ||||||
| import type { AuthState } from "../../utils/AuthService"; | import type { AuthState } from "../../utils/AuthService"; | ||||||
| import LiteElement, { html } from "../../utils/LiteElement"; | import LiteElement, { html } from "../../utils/LiteElement"; | ||||||
| import type { InitialCrawlTemplate } from "./crawl-templates-new"; | import type { InitialCrawlTemplate } from "./crawl-templates-new"; | ||||||
| import type { CrawlTemplate, CrawlConfig } from "./types"; | import type { CrawlTemplate, CrawlConfig } from "./types"; | ||||||
| import { getUTCSchedule } from "./utils"; | import { getUTCSchedule, humanizeSchedule } from "../../utils/cron"; | ||||||
| import "../../components/crawl-scheduler"; | import "../../components/crawl-scheduler"; | ||||||
| 
 | 
 | ||||||
| const SEED_URLS_MAX = 3; | const SEED_URLS_MAX = 3; | ||||||
| @ -707,16 +706,7 @@ export class CrawlTemplatesDetail extends LiteElement { | |||||||
|             ${this.crawlTemplate |             ${this.crawlTemplate | ||||||
|               ? html` |               ? html` | ||||||
|                   ${this.crawlTemplate.schedule |                   ${this.crawlTemplate.schedule | ||||||
|                     ? // TODO localize
 |                     ? humanizeSchedule(this.crawlTemplate.schedule) | ||||||
|                       // NOTE human-readable string is in UTC, limitation of library
 |  | ||||||
|                       // currently being used.
 |  | ||||||
|                       // https://github.com/bradymholt/cRonstrue/issues/94
 |  | ||||||
|                       html`<span
 |  | ||||||
|                         >${cronstrue.toString(this.crawlTemplate.schedule, { |  | ||||||
|                           verbose: true, |  | ||||||
|                         })} |  | ||||||
|                         (in UTC time zone)</span |  | ||||||
|                       >` |  | ||||||
|                     : html`<span class="text-0-400">${msg("None")}</span>`} |                     : html`<span class="text-0-400">${msg("None")}</span>`} | ||||||
|                 ` |                 ` | ||||||
|               : html`<sl-skeleton></sl-skeleton>`} |               : html`<sl-skeleton></sl-skeleton>`} | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| import type { HTMLTemplateResult } from "lit"; | import type { HTMLTemplateResult } from "lit"; | ||||||
| import { state, property } from "lit/decorators.js"; | import { state, property } from "lit/decorators.js"; | ||||||
| import { msg, localized, str } from "@lit/localize"; | import { msg, localized, str } from "@lit/localize"; | ||||||
| import cronParser from "cron-parser"; | import { parseCron } from "@cheap-glitch/mi-cron"; | ||||||
| import debounce from "lodash/fp/debounce"; | import debounce from "lodash/fp/debounce"; | ||||||
| import flow from "lodash/fp/flow"; | import flow from "lodash/fp/flow"; | ||||||
| import map from "lodash/fp/map"; | import map from "lodash/fp/map"; | ||||||
| @ -13,7 +13,11 @@ import type { AuthState } from "../../utils/AuthService"; | |||||||
| import LiteElement, { html } from "../../utils/LiteElement"; | import LiteElement, { html } from "../../utils/LiteElement"; | ||||||
| import type { InitialCrawlTemplate } from "./crawl-templates-new"; | import type { InitialCrawlTemplate } from "./crawl-templates-new"; | ||||||
| import type { CrawlTemplate } from "./types"; | import type { CrawlTemplate } from "./types"; | ||||||
| import { getUTCSchedule } from "./utils"; | import { | ||||||
|  |   getUTCSchedule, | ||||||
|  |   humanizeNextDate, | ||||||
|  |   humanizeSchedule, | ||||||
|  | } from "../../utils/cron"; | ||||||
| import "../../components/crawl-scheduler"; | import "../../components/crawl-scheduler"; | ||||||
| 
 | 
 | ||||||
| type RunningCrawlsMap = { | type RunningCrawlsMap = { | ||||||
| @ -359,7 +363,6 @@ export class CrawlTemplatesList extends LiteElement { | |||||||
|                       year="2-digit" |                       year="2-digit" | ||||||
|                       hour="numeric" |                       hour="numeric" | ||||||
|                       minute="numeric" |                       minute="numeric" | ||||||
|                       time-zone-name="short" |  | ||||||
|                     ></sl-format-date> |                     ></sl-format-date> | ||||||
|                   </a> |                   </a> | ||||||
|                 </sl-tooltip>` |                 </sl-tooltip>` | ||||||
| @ -376,27 +379,21 @@ export class CrawlTemplatesList extends LiteElement { | |||||||
|           <div> |           <div> | ||||||
|             ${t.schedule |             ${t.schedule | ||||||
|               ? html` |               ? html` | ||||||
|                   <sl-tooltip content=${msg("Next scheduled crawl")}> |                   <sl-tooltip | ||||||
|  |                     content=${msg( | ||||||
|  |                       str`Next scheduled crawl: ${humanizeNextDate(t.schedule)}` | ||||||
|  |                     )} | ||||||
|  |                   > | ||||||
|                     <span> |                     <span> | ||||||
|                       <sl-icon |                       <sl-icon | ||||||
|                         class="inline-block align-middle mr-1" |                         class="inline-block align-middle mr-1" | ||||||
|                         name="clock-history" |                         name="clock-history" | ||||||
|                       ></sl-icon |                       ></sl-icon | ||||||
|                       ><sl-format-date |                       ><span class="inline-block align-middle text-0-600" | ||||||
|                         class="inline-block align-middle text-0-600" |                         >${humanizeSchedule(t.schedule, { | ||||||
|                         date="${cronParser |                           length: "short", | ||||||
|                           .parseExpression(t.schedule, { |                         })}</span | ||||||
|                             utc: true, |                       > | ||||||
|                           }) |  | ||||||
|                           .next() |  | ||||||
|                           .toString()}" |  | ||||||
|                         month="2-digit" |  | ||||||
|                         day="2-digit" |  | ||||||
|                         year="2-digit" |  | ||||||
|                         hour="numeric" |  | ||||||
|                         minute="numeric" |  | ||||||
|                         time-zone-name="short" |  | ||||||
|                       ></sl-format-date> |  | ||||||
|                     </span> |                     </span> | ||||||
|                   </sl-tooltip> |                   </sl-tooltip> | ||||||
|                 ` |                 ` | ||||||
|  | |||||||
| @ -1,14 +1,13 @@ | |||||||
| import { state, property } from "lit/decorators.js"; | import { state, property } from "lit/decorators.js"; | ||||||
| import { ifDefined } from "lit/directives/if-defined.js"; | import { ifDefined } from "lit/directives/if-defined.js"; | ||||||
| import { msg, localized, str } from "@lit/localize"; | import { msg, localized, str } from "@lit/localize"; | ||||||
| import cronParser from "cron-parser"; |  | ||||||
| import { parse as yamlToJson, stringify as jsonToYaml } from "yaml"; | import { parse as yamlToJson, stringify as jsonToYaml } from "yaml"; | ||||||
| 
 | 
 | ||||||
| import type { AuthState } from "../../utils/AuthService"; | import type { AuthState } from "../../utils/AuthService"; | ||||||
| import LiteElement, { html } from "../../utils/LiteElement"; | import LiteElement, { html } from "../../utils/LiteElement"; | ||||||
| import { getLocaleTimeZone } from "../../utils/localization"; | import { ScheduleInterval, humanizeNextDate } from "../../utils/cron"; | ||||||
| import type { CrawlConfig, Profile } from "./types"; | import type { CrawlConfig, Profile } from "./types"; | ||||||
| import { getUTCSchedule } from "./utils"; | import { getUTCSchedule } from "../../utils/cron"; | ||||||
| 
 | 
 | ||||||
| type NewCrawlTemplate = { | type NewCrawlTemplate = { | ||||||
|   id?: string; |   id?: string; | ||||||
| @ -65,7 +64,7 @@ export class CrawlTemplatesNew extends LiteElement { | |||||||
|   private isRunNow: boolean = initialValues.runNow; |   private isRunNow: boolean = initialValues.runNow; | ||||||
| 
 | 
 | ||||||
|   @state() |   @state() | ||||||
|   private scheduleInterval: "" | "daily" | "weekly" | "monthly" = ""; |   private scheduleInterval: ScheduleInterval | "" = ""; | ||||||
| 
 | 
 | ||||||
|   /** Schedule local time */ |   /** Schedule local time */ | ||||||
|   @state() |   @state() | ||||||
| @ -92,35 +91,10 @@ export class CrawlTemplatesNew extends LiteElement { | |||||||
|   @state() |   @state() | ||||||
|   private serverError?: string; |   private serverError?: string; | ||||||
| 
 | 
 | ||||||
|   private get timeZone() { |  | ||||||
|     return Intl.DateTimeFormat().resolvedOptions().timeZone; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   private get timeZoneShortName() { |  | ||||||
|     return getLocaleTimeZone(); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   private get formattededNextCrawlDate() { |   private get formattededNextCrawlDate() { | ||||||
|     const utcSchedule = this.getUTCSchedule(); |     const utcSchedule = this.getUTCSchedule(); | ||||||
| 
 | 
 | ||||||
|     return this.scheduleInterval |     return this.scheduleInterval ? humanizeNextDate(utcSchedule) : undefined; | ||||||
|       ? html`<sl-format-date
 |  | ||||||
|           date="${cronParser |  | ||||||
|             .parseExpression(utcSchedule, { |  | ||||||
|               utc: true, |  | ||||||
|             }) |  | ||||||
|             .next() |  | ||||||
|             .toString()}" |  | ||||||
|           weekday="long" |  | ||||||
|           month="long" |  | ||||||
|           day="numeric" |  | ||||||
|           year="numeric" |  | ||||||
|           hour="numeric" |  | ||||||
|           minute="numeric" |  | ||||||
|           time-zone-name="short" |  | ||||||
|           time-zone=${this.timeZone} |  | ||||||
|         ></sl-format-date>` |  | ||||||
|       : undefined; |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   connectedCallback(): void { |   connectedCallback(): void { | ||||||
|  | |||||||
| @ -1,38 +0,0 @@ | |||||||
| /** |  | ||||||
|  * Get schedule as UTC cron job expression |  | ||||||
|  * https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/#cron-schedule-syntax
 |  | ||||||
|  **/ |  | ||||||
| export function getUTCSchedule({ |  | ||||||
|   interval, |  | ||||||
|   minute, |  | ||||||
|   hour, |  | ||||||
|   period, |  | ||||||
| }: { |  | ||||||
|   interval: "daily" | "weekly" | "monthly"; |  | ||||||
|   minute: number | string; |  | ||||||
|   hour: number | string; |  | ||||||
|   period: "AM" | "PM"; |  | ||||||
| }): string { |  | ||||||
|   const localDate = new Date(); |  | ||||||
| 
 |  | ||||||
|   // Convert 12-hr to 24-hr time
 |  | ||||||
|   let periodOffset = 0; |  | ||||||
| 
 |  | ||||||
|   if (hour === 12) { |  | ||||||
|     if (period === "AM") { |  | ||||||
|       periodOffset = -12; |  | ||||||
|     } |  | ||||||
|   } else if (period === "PM") { |  | ||||||
|     periodOffset = 12; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   localDate.setHours(+hour + periodOffset); |  | ||||||
|   localDate.setMinutes(+minute); |  | ||||||
|   const dayOfMonth = interval === "monthly" ? localDate.getUTCDate() : "*"; |  | ||||||
|   const dayOfWeek = interval === "weekly" ? localDate.getUTCDay() : "*"; |  | ||||||
|   const month = "*"; |  | ||||||
| 
 |  | ||||||
|   const schedule = `${localDate.getUTCMinutes()} ${localDate.getUTCHours()} ${dayOfMonth} ${month} ${dayOfWeek}`; |  | ||||||
| 
 |  | ||||||
|   return schedule; |  | ||||||
| } |  | ||||||
							
								
								
									
										145
									
								
								frontend/src/utils/cron.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								frontend/src/utils/cron.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,145 @@ | |||||||
|  | import { parseCron } from "@cheap-glitch/mi-cron"; | ||||||
|  | import { msg, str } from "@lit/localize"; | ||||||
|  | 
 | ||||||
|  | export type ScheduleInterval = "daily" | "weekly" | "monthly"; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Parse interval from cron expression | ||||||
|  |  **/ | ||||||
|  | export function getScheduleInterval( | ||||||
|  |   schedule: string | ||||||
|  | ): "daily" | "weekly" | "monthly" { | ||||||
|  |   const [minute, hour, dayofMonth, month, dayOfWeek] = schedule.split(" "); | ||||||
|  |   if (dayofMonth === "*") { | ||||||
|  |     if (dayOfWeek === "*") { | ||||||
|  |       return "daily"; | ||||||
|  |     } | ||||||
|  |     return "weekly"; | ||||||
|  |   } | ||||||
|  |   return "monthly"; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Get human-friendly date from cron expression | ||||||
|  |  * Example: "Every day at 9:30 AM CDT" | ||||||
|  |  **/ | ||||||
|  | export function humanizeNextDate(schedule: string): string { | ||||||
|  |   const nextDate = parseCron.nextDate(schedule); | ||||||
|  | 
 | ||||||
|  |   if (!nextDate) return ""; | ||||||
|  | 
 | ||||||
|  |   return nextDate.toLocaleString(undefined, { | ||||||
|  |     weekday: "long", | ||||||
|  |     month: "long", | ||||||
|  |     day: "numeric", | ||||||
|  |     year: "numeric", | ||||||
|  |     hour: "numeric", | ||||||
|  |     minute: "numeric", | ||||||
|  |     timeZoneName: "short", | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Get human-friendly schedule from cron expression | ||||||
|  |  * Example: "Every day at 9:30 AM CDT" | ||||||
|  |  **/ | ||||||
|  | export function humanizeSchedule( | ||||||
|  |   schedule: string, | ||||||
|  |   options: { length?: "short" } = {} | ||||||
|  | ): string { | ||||||
|  |   const interval = getScheduleInterval(schedule); | ||||||
|  |   const { days } = parseCron(schedule)!; | ||||||
|  |   const nextDate = parseCron.nextDate(schedule)!; | ||||||
|  |   const formattedWeekDay = nextDate.toLocaleString(undefined, { | ||||||
|  |     weekday: "long", | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   let intervalMsg: any = ""; | ||||||
|  | 
 | ||||||
|  |   if (options.length === "short") { | ||||||
|  |     const formattedTime = nextDate.toLocaleString(undefined, { | ||||||
|  |       minute: "numeric", | ||||||
|  |       hour: "numeric", | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     switch (interval) { | ||||||
|  |       case "daily": | ||||||
|  |         intervalMsg = msg(str`${formattedTime} every day`); | ||||||
|  |         break; | ||||||
|  |       case "weekly": | ||||||
|  |         intervalMsg = msg(str`Every ${formattedWeekDay}`); | ||||||
|  |         break; | ||||||
|  |       case "monthly": | ||||||
|  |         intervalMsg = msg(str`Day ${days[0]} of every month`); | ||||||
|  |         break; | ||||||
|  |       default: | ||||||
|  |         break; | ||||||
|  |     } | ||||||
|  |   } else { | ||||||
|  |     const formattedTime = nextDate.toLocaleString(undefined, { | ||||||
|  |       minute: "numeric", | ||||||
|  |       hour: "numeric", | ||||||
|  |       timeZoneName: "short", | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     switch (interval) { | ||||||
|  |       case "daily": | ||||||
|  |         intervalMsg = msg(str`Every day at ${formattedTime}`); | ||||||
|  |         break; | ||||||
|  |       case "weekly": | ||||||
|  |         intervalMsg = msg( | ||||||
|  |           str`Every ${formattedWeekDay} | ||||||
|  |             at ${formattedTime}` | ||||||
|  |         ); | ||||||
|  |         break; | ||||||
|  |       case "monthly": | ||||||
|  |         intervalMsg = msg( | ||||||
|  |           str`On day ${days[0]} of the month at ${formattedTime}` | ||||||
|  |         ); | ||||||
|  |         break; | ||||||
|  |       default: | ||||||
|  |         break; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return intervalMsg; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Get schedule as UTC cron job expression | ||||||
|  |  * https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/#cron-schedule-syntax
 | ||||||
|  |  **/ | ||||||
|  | export function getUTCSchedule({ | ||||||
|  |   interval, | ||||||
|  |   minute, | ||||||
|  |   hour, | ||||||
|  |   period, | ||||||
|  | }: { | ||||||
|  |   interval: ScheduleInterval; | ||||||
|  |   minute: number | string; | ||||||
|  |   hour: number | string; | ||||||
|  |   period: "AM" | "PM"; | ||||||
|  | }): string { | ||||||
|  |   const localDate = new Date(); | ||||||
|  | 
 | ||||||
|  |   // Convert 12-hr to 24-hr time
 | ||||||
|  |   let periodOffset = 0; | ||||||
|  | 
 | ||||||
|  |   if (hour === 12) { | ||||||
|  |     if (period === "AM") { | ||||||
|  |       periodOffset = -12; | ||||||
|  |     } | ||||||
|  |   } else if (period === "PM") { | ||||||
|  |     periodOffset = 12; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   localDate.setHours(+hour + periodOffset); | ||||||
|  |   localDate.setMinutes(+minute); | ||||||
|  |   const dayOfMonth = interval === "monthly" ? localDate.getUTCDate() : "*"; | ||||||
|  |   const dayOfWeek = interval === "weekly" ? localDate.getUTCDay() : "*"; | ||||||
|  |   const month = "*"; | ||||||
|  | 
 | ||||||
|  |   const schedule = `${localDate.getUTCMinutes()} ${localDate.getUTCHours()} ${dayOfMonth} ${month} ${dayOfWeek}`; | ||||||
|  | 
 | ||||||
|  |   return schedule; | ||||||
|  | } | ||||||
| @ -39,6 +39,11 @@ | |||||||
|     chalk "^2.0.0" |     chalk "^2.0.0" | ||||||
|     js-tokens "^4.0.0" |     js-tokens "^4.0.0" | ||||||
| 
 | 
 | ||||||
|  | "@cheap-glitch/mi-cron@^1.0.1": | ||||||
|  |   version "1.0.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@cheap-glitch/mi-cron/-/mi-cron-1.0.1.tgz#111f4ce746c269aedf74533ac806881763a68f99" | ||||||
|  |   integrity sha512-kxl7vhg+SUgyHRn22qVbR9MfSm5CzdlYZDJTbGemqFFi/Jmno/hdoQIvBIPoqFY9dcPyxzOUNRRFn6x88UQMpw== | ||||||
|  | 
 | ||||||
| "@discoveryjs/json-ext@^0.5.0": | "@discoveryjs/json-ext@^0.5.0": | ||||||
|   version "0.5.5" |   version "0.5.5" | ||||||
|   resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.5.tgz#9283c9ce5b289a3c4f61c12757469e59377f81f3" |   resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.5.tgz#9283c9ce5b289a3c4f61c12757469e59377f81f3" | ||||||
| @ -1774,18 +1779,6 @@ cosmiconfig@^7.0.0, cosmiconfig@^7.0.1: | |||||||
|     path-type "^4.0.0" |     path-type "^4.0.0" | ||||||
|     yaml "^1.10.0" |     yaml "^1.10.0" | ||||||
| 
 | 
 | ||||||
| cron-parser@^4.2.1: |  | ||||||
|   version "4.2.1" |  | ||||||
|   resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-4.2.1.tgz#b43205d05ccd5c93b097dae64f3bd811f5993af3" |  | ||||||
|   integrity sha512-5sJBwDYyCp+0vU5b7POl8zLWfgV5fOHxlc45FWoWdHecGC7MQHCjx0CHivCMRnGFovghKhhyYM+Zm9DcY5qcHg== |  | ||||||
|   dependencies: |  | ||||||
|     luxon "^1.28.0" |  | ||||||
| 
 |  | ||||||
| cronstrue@^1.123.0: |  | ||||||
|   version "1.123.0" |  | ||||||
|   resolved "https://registry.yarnpkg.com/cronstrue/-/cronstrue-1.123.0.tgz#de622dd8ea07981790f488d3f89a25e6e728ac8b" |  | ||||||
|   integrity sha512-hVu9yNYRYr+jj5KET1p7FaBxFwtCHM1ByffP9lZ6yJ6p53u4VEmzH8117v9PUydxWNzc8Eq+sCZEzsKcB3ckiA== |  | ||||||
| 
 |  | ||||||
| cross-spawn@^7.0.2, cross-spawn@^7.0.3: | cross-spawn@^7.0.2, cross-spawn@^7.0.3: | ||||||
|   version "7.0.3" |   version "7.0.3" | ||||||
|   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" |   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" | ||||||
| @ -3558,11 +3551,6 @@ lru-cache@^6.0.0: | |||||||
|   dependencies: |   dependencies: | ||||||
|     yallist "^4.0.0" |     yallist "^4.0.0" | ||||||
| 
 | 
 | ||||||
| luxon@^1.28.0: |  | ||||||
|   version "1.28.0" |  | ||||||
|   resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.28.0.tgz#e7f96daad3938c06a62de0fb027115d251251fbf" |  | ||||||
|   integrity sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ== |  | ||||||
| 
 |  | ||||||
| make-dir@^3.0.0: | make-dir@^3.0.0: | ||||||
|   version "3.1.0" |   version "3.1.0" | ||||||
|   resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" |   resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user