parent
							
								
									c2aa4e6319
								
							
						
					
					
						commit
						d144591dbf
					
				| @ -5,6 +5,7 @@ | ||||
|   "license": "MIT", | ||||
|   "private": true, | ||||
|   "dependencies": { | ||||
|     "@cheap-glitch/mi-cron": "^1.0.1", | ||||
|     "@formatjs/intl-displaynames": "^5.2.5", | ||||
|     "@formatjs/intl-getcanonicallocales": "^1.8.0", | ||||
|     "@lit/localize": "^0.11.1", | ||||
| @ -12,8 +13,6 @@ | ||||
|     "@xstate/fsm": "^1.6.2", | ||||
|     "axios": "^0.22.0", | ||||
|     "color": "^4.0.1", | ||||
|     "cron-parser": "^4.2.1", | ||||
|     "cronstrue": "^1.123.0", | ||||
|     "fuse.js": "^6.5.3", | ||||
|     "lit": "^2.0.0", | ||||
|     "lodash": "^4.17.21", | ||||
|  | ||||
| @ -1,14 +1,25 @@ | ||||
| import { state, property } from "lit/decorators.js"; | ||||
| 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 { getLocaleTimeZone } from "../utils/localization"; | ||||
| import { | ||||
|   ScheduleInterval, | ||||
|   getScheduleInterval, | ||||
|   getUTCSchedule, | ||||
|   humanizeSchedule, | ||||
|   humanizeNextDate, | ||||
| } from "../utils/cron"; | ||||
| import type { CrawlTemplate } from "../pages/archive/types"; | ||||
| 
 | ||||
| const nowHour = new Date().getHours(); | ||||
| const initialHours = nowHour % 12 || 12; | ||||
| const initialPeriod = nowHour > 11 ? "PM" : "AM"; | ||||
| 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"), | ||||
| })); | ||||
| 
 | ||||
| /** | ||||
|  * Usage: | ||||
| @ -36,49 +47,28 @@ export class CrawlTemplatesScheduler extends LiteElement { | ||||
|   cancelable?: boolean = false; | ||||
| 
 | ||||
|   @state() | ||||
|   private editedSchedule?: string; | ||||
|   private scheduleInterval: ScheduleInterval | "" = ""; | ||||
| 
 | ||||
|   @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 schedulePeriod: "AM" | "PM" = initialPeriod; | ||||
|   private get isScheduleDisabled(): boolean { | ||||
|     return !this.scheduleInterval; | ||||
|   } | ||||
| 
 | ||||
|   private get timeZoneShortName() { | ||||
|     return getLocaleTimeZone(); | ||||
|   firstUpdated() { | ||||
|     this.setInitialValues(); | ||||
|   } | ||||
| 
 | ||||
|   render() { | ||||
|     // 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 [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]; | ||||
|     const utcSchedule = this.getUTCSchedule(); | ||||
| 
 | ||||
|     return html` | ||||
|       <sl-form @sl-submit=${this.onSubmit}> | ||||
| @ -87,23 +77,12 @@ export class CrawlTemplatesScheduler extends LiteElement { | ||||
|             <sl-select | ||||
|               name="scheduleInterval" | ||||
|               label=${msg("Recurring crawls")} | ||||
|               value=${initialInterval} | ||||
|               value=${this.scheduleInterval} | ||||
|               hoist | ||||
|               @sl-hide=${this.stopProp} | ||||
|               @sl-after-hide=${this.stopProp} | ||||
|               @sl-select=${(e: any) => { | ||||
|                 if (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; | ||||
|                 } | ||||
|                 this.scheduleInterval = e.target.value; | ||||
|               }} | ||||
|             > | ||||
|               <sl-menu-item value="">${msg("None")}</sl-menu-item> | ||||
| @ -120,20 +99,16 @@ export class CrawlTemplatesScheduler extends LiteElement { | ||||
|               <sl-select | ||||
|                 class="grow" | ||||
|                 name="scheduleHour" | ||||
|                 value=${initialHours} | ||||
|                 value=${this.scheduleTime.hour} | ||||
|                 ?disabled=${this.isScheduleDisabled} | ||||
|                 hoist | ||||
|                 @sl-hide=${this.stopProp} | ||||
|                 @sl-after-hide=${this.stopProp} | ||||
|                 @sl-select=${(e: any) => { | ||||
|                   const hour = +e.target.value; | ||||
|                   const period = this.schedulePeriod; | ||||
| 
 | ||||
|                   this.setScheduleHour({ | ||||
|                     hour, | ||||
|                     period, | ||||
|                     schedule: nextSchedule, | ||||
|                   }); | ||||
|                   this.scheduleTime = { | ||||
|                     ...this.scheduleTime, | ||||
|                     hour: +e.target.value, | ||||
|                   }; | ||||
|                 }} | ||||
|               > | ||||
|                 ${hours.map( | ||||
| @ -145,16 +120,16 @@ export class CrawlTemplatesScheduler extends LiteElement { | ||||
|               <sl-select | ||||
|                 class="grow" | ||||
|                 name="scheduleMinute" | ||||
|                 value="0" | ||||
|                 value=${this.scheduleTime.minute} | ||||
|                 ?disabled=${this.isScheduleDisabled} | ||||
|                 hoist | ||||
|                 @sl-hide=${this.stopProp} | ||||
|                 @sl-after-hide=${this.stopProp} | ||||
|                 @sl-select=${(e: any) => | ||||
|                   (this.editedSchedule = `${e.target.value} ${nextSchedule | ||||
|                     .split(" ") | ||||
|                     .slice(1) | ||||
|                     .join(" ")}`)}
 | ||||
|                   (this.scheduleTime = { | ||||
|                     ...this.scheduleTime, | ||||
|                     minute: +e.target.value, | ||||
|                   })} | ||||
|               > | ||||
|                 ${minutes.map( | ||||
|                   ({ value, label }) => | ||||
| @ -165,66 +140,52 @@ export class CrawlTemplatesScheduler extends LiteElement { | ||||
|             <input | ||||
|               name="schedulePeriod" | ||||
|               type="hidden" | ||||
|               value=${this.schedulePeriod} | ||||
|               value=${this.scheduleTime.period} | ||||
|             /> | ||||
|             <sl-button-group> | ||||
|               <sl-button | ||||
|                 type=${this.schedulePeriod === "AM" ? "neutral" : "default"} | ||||
|                 aria-selected=${this.schedulePeriod === "AM"} | ||||
|                 type=${this.scheduleTime.period === "AM" | ||||
|                   ? "neutral" | ||||
|                   : "default"} | ||||
|                 aria-selected=${this.scheduleTime.period === "AM"} | ||||
|                 ?disabled=${this.isScheduleDisabled} | ||||
|                 @click=${(e: any) => { | ||||
|                   const hour = +e.target | ||||
|                     .closest("sl-form") | ||||
|                     .querySelector('sl-select[name="scheduleHour"]').value; | ||||
|                   const period = "AM"; | ||||
| 
 | ||||
|                   this.schedulePeriod = period; | ||||
|                   this.setScheduleHour({ | ||||
|                     hour, | ||||
|                     period, | ||||
|                     schedule: nextSchedule, | ||||
|                   }); | ||||
|                 }} | ||||
|                 @click=${() => | ||||
|                   (this.scheduleTime = { | ||||
|                     ...this.scheduleTime, | ||||
|                     period: "AM", | ||||
|                   })} | ||||
|                 >${msg("AM", { desc: "Time AM/PM" })}</sl-button | ||||
|               > | ||||
|               <sl-button | ||||
|                 type=${this.schedulePeriod === "PM" ? "neutral" : "default"} | ||||
|                 aria-selected=${this.schedulePeriod === "PM"} | ||||
|                 type=${this.scheduleTime.period === "PM" | ||||
|                   ? "neutral" | ||||
|                   : "default"} | ||||
|                 aria-selected=${this.scheduleTime.period === "PM"} | ||||
|                 ?disabled=${this.isScheduleDisabled} | ||||
|                 @click=${(e: any) => { | ||||
|                   const hour = +e.target | ||||
|                     .closest("sl-form") | ||||
|                     .querySelector('sl-select[name="scheduleHour"]').value; | ||||
|                   const period = "PM"; | ||||
| 
 | ||||
|                   this.schedulePeriod = period; | ||||
|                   this.setScheduleHour({ | ||||
|                     hour, | ||||
|                     period, | ||||
|                     schedule: nextSchedule, | ||||
|                   }); | ||||
|                 }} | ||||
|                 @click=${() => | ||||
|                   (this.scheduleTime = { | ||||
|                     ...this.scheduleTime, | ||||
|                     period: "PM", | ||||
|                   })} | ||||
|                 >${msg("PM", { desc: "Time AM/PM" })}</sl-button | ||||
|               > | ||||
|             </sl-button-group> | ||||
|           </div> | ||||
|         </fieldset> | ||||
| 
 | ||||
|         <div class="mt-5"> | ||||
|         <div class="mt-5 bg-neutral-50 rounded p-3 text-sm text-neutral-800"> | ||||
|           ${this.isScheduleDisabled | ||||
|             ? msg(html`<span class="font-medium"
 | ||||
|                 >Crawls will not repeat.</span | ||||
|               >`)
 | ||||
|             : msg( | ||||
|                 html`<span class="font-medium">New schedule will be:</span
 | ||||
|                   ><br /> | ||||
|                   <span class="text-0-600" | ||||
|                     >${cronstrue.toString(nextSchedule, { | ||||
|                       verbose: true, | ||||
|                     })} | ||||
|                     (in ${this.timeZoneShortName} time zone)</span | ||||
|                   >` | ||||
|               )} | ||||
|             ? html`<span class="font-medium"
 | ||||
|                 >${msg("Crawls will not repeat.")}</span | ||||
|               >` | ||||
|             : html` | ||||
|                 <p>${msg(str`Schedule: ${humanizeSchedule(utcSchedule)}.`)}</p> | ||||
|                 <p> | ||||
|                   ${msg( | ||||
|                     str`Next scheduled run: ${humanizeNextDate(utcSchedule)}.` | ||||
|                   )} | ||||
|                 </p> | ||||
|               `}
 | ||||
|         </div> | ||||
| 
 | ||||
|         <div class="mt-5${this.cancelable ? " text-right" : ""}"> | ||||
| @ -256,34 +217,6 @@ export class CrawlTemplatesScheduler extends LiteElement { | ||||
|     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. | ||||
|    * Prevents bug where sl-dialog closes when dropdown closes | ||||
| @ -292,6 +225,29 @@ export class CrawlTemplatesScheduler extends LiteElement { | ||||
|   private stopProp(e: CustomEvent) { | ||||
|     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); | ||||
|  | ||||
| @ -2,14 +2,13 @@ import type { HTMLTemplateResult } from "lit"; | ||||
| import { state, property } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
| import { msg, localized, str } from "@lit/localize"; | ||||
| import cronstrue from "cronstrue"; // TODO localize
 | ||||
| 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 { getUTCSchedule, humanizeSchedule } from "../../utils/cron"; | ||||
| import "../../components/crawl-scheduler"; | ||||
| 
 | ||||
| const SEED_URLS_MAX = 3; | ||||
| @ -707,16 +706,7 @@ export class CrawlTemplatesDetail extends LiteElement { | ||||
|             ${this.crawlTemplate | ||||
|               ? html` | ||||
|                   ${this.crawlTemplate.schedule | ||||
|                     ? // TODO localize
 | ||||
|                       // 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 | ||||
|                       >` | ||||
|                     ? humanizeSchedule(this.crawlTemplate.schedule) | ||||
|                     : html`<span class="text-0-400">${msg("None")}</span>`} | ||||
|                 ` | ||||
|               : html`<sl-skeleton></sl-skeleton>`} | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| import type { HTMLTemplateResult } from "lit"; | ||||
| import { state, property } from "lit/decorators.js"; | ||||
| 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 flow from "lodash/fp/flow"; | ||||
| import map from "lodash/fp/map"; | ||||
| @ -13,7 +13,11 @@ 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 { | ||||
|   getUTCSchedule, | ||||
|   humanizeNextDate, | ||||
|   humanizeSchedule, | ||||
| } from "../../utils/cron"; | ||||
| import "../../components/crawl-scheduler"; | ||||
| 
 | ||||
| type RunningCrawlsMap = { | ||||
| @ -359,7 +363,6 @@ export class CrawlTemplatesList extends LiteElement { | ||||
|                       year="2-digit" | ||||
|                       hour="numeric" | ||||
|                       minute="numeric" | ||||
|                       time-zone-name="short" | ||||
|                     ></sl-format-date> | ||||
|                   </a> | ||||
|                 </sl-tooltip>` | ||||
| @ -376,27 +379,21 @@ export class CrawlTemplatesList extends LiteElement { | ||||
|           <div> | ||||
|             ${t.schedule | ||||
|               ? html` | ||||
|                   <sl-tooltip content=${msg("Next scheduled crawl")}> | ||||
|                   <sl-tooltip | ||||
|                     content=${msg( | ||||
|                       str`Next scheduled crawl: ${humanizeNextDate(t.schedule)}` | ||||
|                     )} | ||||
|                   > | ||||
|                     <span> | ||||
|                       <sl-icon | ||||
|                         class="inline-block align-middle mr-1" | ||||
|                         name="clock-history" | ||||
|                       ></sl-icon | ||||
|                       ><sl-format-date | ||||
|                         class="inline-block align-middle text-0-600" | ||||
|                         date="${cronParser | ||||
|                           .parseExpression(t.schedule, { | ||||
|                             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 class="inline-block align-middle text-0-600" | ||||
|                         >${humanizeSchedule(t.schedule, { | ||||
|                           length: "short", | ||||
|                         })}</span | ||||
|                       > | ||||
|                     </span> | ||||
|                   </sl-tooltip> | ||||
|                 ` | ||||
|  | ||||
| @ -1,14 +1,13 @@ | ||||
| import { state, property } from "lit/decorators.js"; | ||||
| 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 type { AuthState } from "../../utils/AuthService"; | ||||
| import LiteElement, { html } from "../../utils/LiteElement"; | ||||
| import { getLocaleTimeZone } from "../../utils/localization"; | ||||
| import { ScheduleInterval, humanizeNextDate } from "../../utils/cron"; | ||||
| import type { CrawlConfig, Profile } from "./types"; | ||||
| import { getUTCSchedule } from "./utils"; | ||||
| import { getUTCSchedule } from "../../utils/cron"; | ||||
| 
 | ||||
| type NewCrawlTemplate = { | ||||
|   id?: string; | ||||
| @ -65,7 +64,7 @@ export class CrawlTemplatesNew extends LiteElement { | ||||
|   private isRunNow: boolean = initialValues.runNow; | ||||
| 
 | ||||
|   @state() | ||||
|   private scheduleInterval: "" | "daily" | "weekly" | "monthly" = ""; | ||||
|   private scheduleInterval: ScheduleInterval | "" = ""; | ||||
| 
 | ||||
|   /** Schedule local time */ | ||||
|   @state() | ||||
| @ -92,35 +91,10 @@ export class CrawlTemplatesNew extends LiteElement { | ||||
|   @state() | ||||
|   private serverError?: string; | ||||
| 
 | ||||
|   private get timeZone() { | ||||
|     return Intl.DateTimeFormat().resolvedOptions().timeZone; | ||||
|   } | ||||
| 
 | ||||
|   private get timeZoneShortName() { | ||||
|     return getLocaleTimeZone(); | ||||
|   } | ||||
| 
 | ||||
|   private get formattededNextCrawlDate() { | ||||
|     const utcSchedule = this.getUTCSchedule(); | ||||
| 
 | ||||
|     return this.scheduleInterval | ||||
|       ? 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; | ||||
|     return this.scheduleInterval ? humanizeNextDate(utcSchedule) : undefined; | ||||
|   } | ||||
| 
 | ||||
|   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" | ||||
|     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": | ||||
|   version "0.5.5" | ||||
|   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" | ||||
|     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: | ||||
|   version "7.0.3" | ||||
|   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" | ||||
| @ -3558,11 +3551,6 @@ lru-cache@^6.0.0: | ||||
|   dependencies: | ||||
|     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: | ||||
|   version "3.1.0" | ||||
|   resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user