Cron Job Scheduler
blockdevopsTemporal and AWS EventBridge style scheduled tasks and background cron worker manager with KPI overview, status indicators, execution details, action menus, and an interactive cron expression builder with upcoming run previews.
Installation
$pnpm dlx uipkge-ng@latest add cron-job-scheduler$npx uipkge-ng@latest add cron-job-scheduler$yarn dlx uipkge-ng@latest add cron-job-scheduler$bunx uipkge-ng@latest add cron-job-schedulerManifest:
https://uipkge.dev/r/angular/cron-job-scheduler.jsonInstalls to:src/app/components/blocks/cron-job-scheduler/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
LastExecutioninterface LastExecution {
status: 'success' | 'error'
code: string
duration: string
timestamp: string
}CronJobinterface CronJob {
id: string
name: string
target: string
cronExpression: string
humanSchedule: string
timezone: string
status: JobStatus
nextRun: string
lastExecution: LastExecution
}CronStatsinterface CronStats {
totalScheduled: number
activeWorkers: number
failed24h: number
executionsToday: string
}npm dependencies
Files installed (2)
src/app/components/blocks/cron-job-scheduler/cron-job-scheduler.component.ts50.2 kB
import { ChangeDetectionStrategy, Component, DestroyRef, Input, computed, inject, signal } from '@angular/core' import { Activity, AlertCircle, AlertTriangle, CalendarClock, Check, CheckCircle2, Clock, Copy, Cpu, Globe, LucideAngularModule, MoreHorizontal, Pause, Pencil, Play, Plus, RefreshCw, Search, Terminal, Trash2, X, XCircle, Zap, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiDialogComponent, UiDialogContentComponent, UiDialogDescriptionComponent, UiDialogFooterComponent, UiDialogHeaderComponent, UiDialogTitleComponent, } from '@/ui/dialog/dialog.component' import { UiDropdownMenuComponent, UiDropdownMenuContentComponent, UiDropdownMenuItemComponent, UiDropdownMenuLabelComponent, UiDropdownMenuSeparatorComponent, UiDropdownMenuTriggerComponent, } from '@/ui/dropdown-menu/dropdown-menu.component' import { UiInputComponent } from '@/ui/input/input.component' import { UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, } from '@/ui/select/select.component' import { UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, } from '@/ui/table/table.component' export type JobStatus = 'active' | 'paused' | 'error' export interface LastExecution { status: 'success' | 'error' code: string duration: string timestamp: string } export interface CronJob { id: string name: string target: string cronExpression: string humanSchedule: string timezone: string status: JobStatus nextRun: string lastExecution: LastExecution } export interface CronStats { totalScheduled: number activeWorkers: number failed24h: number executionsToday: string } export interface CronJobSchedulerProps { initialJobs?: CronJob[] initialStats?: CronStats className?: string } export type CronFrequency = 'minute' | 'hour' | 'day' | 'week' | 'month' const defaultJobs: CronJob[] = [ { id: 'job-1', name: 'daily-billing-reconciliation', target: 'POST https://api.acme.corp/v1/billing/reconcile', cronExpression: '0 0 * * *', humanSchedule: 'Every day at midnight', timezone: 'UTC', status: 'active', nextRun: 'in 4 hours', lastExecution: { status: 'success', code: '200 OK', duration: '1.2s', timestamp: '20 hours ago', }, }, { id: 'job-2', name: 'database-backup-s3', target: 'cron/infra::snapshotPostgres', cronExpression: '0 */6 * * *', humanSchedule: 'Every 6 hours', timezone: 'UTC', status: 'active', nextRun: 'in 1 hour 45 min', lastExecution: { status: 'success', code: '200 OK', duration: '45.3s', timestamp: '4 hours ago', }, }, { id: 'job-3', name: 'cleanup-expired-sessions', target: 'POST https://auth.acme.corp/cron/prune-sessions', cronExpression: '*/30 * * * *', humanSchedule: 'Every 30 minutes', timezone: 'UTC', status: 'active', nextRun: 'in 12 min', lastExecution: { status: 'success', code: '200 OK', duration: '380ms', timestamp: '18 min ago', }, }, { id: 'job-4', name: 'send-digest-emails', target: 'notifications::dispatchWeeklyDigest', cronExpression: '0 9 * * 1', humanSchedule: 'Every Monday at 09:00', timezone: 'UTC', status: 'paused', nextRun: 'Paused', lastExecution: { status: 'success', code: '200 OK', duration: '8.4s', timestamp: '4 days ago', }, }, { id: 'job-5', name: 'sync-crm-contacts', target: 'POST https://integrations.acme.corp/hubspot/sync', cronExpression: '15 * * * *', humanSchedule: 'At minute 15 of every hour', timezone: 'UTC', status: 'error', nextRun: 'in 28 min', lastExecution: { status: 'error', code: '504 Gateway Timeout', duration: '30.0s', timestamp: '32 min ago', }, }, ] const defaultStats: CronStats = { totalScheduled: 14, activeWorkers: 12, failed24h: 1, executionsToday: '48.2k', } const weekDayNames: Record<string, string> = { '0': 'Sunday', '1': 'Monday', '2': 'Tuesday', '3': 'Wednesday', '4': 'Thursday', '5': 'Friday', '6': 'Saturday', } const pad = (v: string) => v.padStart(2, '0') /** * Angular port of the UIPKGE `CronJobScheduler` block (React `CronJobScheduler`): KPI cards, * a filterable scheduled-jobs table with per-row action menus, an interactive cron expression * helper with simulated upcoming runs, and the create-job / execution-logs dialogs. */ @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-cron-job-scheduler, [ui-cron-job-scheduler]', standalone: true, host: { class: 'block' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, UiDialogComponent, UiDialogContentComponent, UiDialogDescriptionComponent, UiDialogFooterComponent, UiDialogHeaderComponent, UiDialogTitleComponent, UiDropdownMenuComponent, UiDropdownMenuContentComponent, UiDropdownMenuItemComponent, UiDropdownMenuLabelComponent, UiDropdownMenuSeparatorComponent, UiDropdownMenuTriggerComponent, UiInputComponent, UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, ], template: ` <div data-slot="cron-job-scheduler" [class]="rootClass"> <!-- Header --> <div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div class="space-y-1"> <div class="flex items-center gap-2"> <lucide-icon [img]="CalendarClock" class="text-primary size-6" /> <h1 class="text-foreground text-2xl font-bold tracking-tight">Cron Jobs & Scheduled Tasks</h1> </div> <p class="text-muted-foreground text-sm"> Configure automated background workers, recurring tasks, and queue schedules. </p> </div> <div class="flex items-center gap-2"> <button ui-button (click)="isCreateOpen.set(true)"> <lucide-icon [img]="Plus" class="mr-1.5 size-4" /> New Cron Job </button> </div> </div> <!-- Notification / Action banner --> @if (bannerMessage(); as banner) { <div class="border-border bg-card flex items-center justify-between gap-3 rounded-lg border p-3 shadow-xs"> <div class="flex items-center gap-2.5 text-sm"> @if (banner.type === 'success') { <lucide-icon [img]="CheckCircle2" class="text-success size-4 shrink-0" /> } @else { <lucide-icon [img]="Zap" class="text-primary size-4 shrink-0" /> } <span class="text-foreground font-medium">{{ banner.text }}</span> </div> <button ui-button variant="ghost" size="icon" class="size-7" (click)="bannerMessage.set(null)"> <lucide-icon [img]="X" class="size-3.5" /> <span class="sr-only">Dismiss</span> </button> </div> } <!-- 4 Stat / KPI Cards --> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> <div ui-card class="shadow-xs"> <div ui-card-header class="flex flex-row items-center justify-between pb-2"> <h3 ui-card-title class="text-muted-foreground text-sm font-medium">Total Scheduled</h3> <div class="bg-primary/10 text-primary rounded-lg p-2"> <lucide-icon [img]="Clock" class="size-4" /> </div> </div> <div ui-card-content class="space-y-1"> <div class="text-foreground text-2xl font-bold tracking-tight">{{ stats.totalScheduled }}</div> <p class="text-muted-foreground text-xs">Across 4 worker clusters</p> </div> </div> <div ui-card class="shadow-xs"> <div ui-card-header class="flex flex-row items-center justify-between pb-2"> <h3 ui-card-title class="text-muted-foreground text-sm font-medium">Active Workers</h3> <div class="bg-success/10 text-success rounded-lg p-2"> <lucide-icon [img]="Cpu" class="size-4" /> </div> </div> <div ui-card-content class="space-y-1"> <div class="text-foreground text-2xl font-bold tracking-tight">{{ stats.activeWorkers }}</div> <p class="text-muted-foreground text-xs">2 currently paused</p> </div> </div> <div ui-card class="border-warning/30 bg-warning/5 bg-warning/10 shadow-xs"> <div ui-card-header class="flex flex-row items-center justify-between pb-2"> <h3 ui-card-title class="text-warning text-sm font-medium">Failed in 24h</h3> <div class="bg-warning/20 text-warning rounded-lg p-2"> <lucide-icon [img]="AlertTriangle" class="size-4" /> </div> </div> <div ui-card-content class="space-y-1"> <div class="flex items-center gap-2"> <span class="text-warning text-2xl font-bold tracking-tight">{{ stats.failed24h }}</span> <span ui-badge variant="warning" class="text-xs">Amber alert</span> </div> <p class="text-warning/90 text-xs">1 job needs attention</p> </div> </div> <div ui-card class="shadow-xs"> <div ui-card-header class="flex flex-row items-center justify-between pb-2"> <h3 ui-card-title class="text-muted-foreground text-sm font-medium">Executions Today</h3> <div class="bg-info/10 text-info rounded-lg p-2"> <lucide-icon [img]="Activity" class="size-4" /> </div> </div> <div ui-card-content class="space-y-1"> <div class="text-foreground text-2xl font-bold tracking-tight">{{ stats.executionsToday }}</div> <p class="text-muted-foreground text-xs">99.8% success rate (18ms avg)</p> </div> </div> </div> <!-- Scheduled Jobs Table Card --> <div ui-card class="shadow-xs"> <div ui-card-header class="pb-3"> <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div> <h3 ui-card-title class="text-lg font-semibold">Scheduled Jobs</h3> <p ui-card-description class="text-xs"> {{ filteredJobs().length }} of {{ jobs().length }} tasks registered in cluster scheduler. </p> </div> <div class="flex flex-wrap items-center gap-2"> <div class="relative w-full sm:w-64"> <lucide-icon [img]="Search" class="text-muted-foreground pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2" /> <ui-input [value]="search()" (valueChange)="search.set($event)" placeholder="Search job name or target..." class="h-8 pl-9 text-xs" /> </div> <ui-select [value]="statusFilter()" (valueChange)="setStatusFilter($event)"> <button ui-select-trigger class="h-8 w-32 text-xs"> <ui-select-value placeholder="Status" /> </button> <ui-select-content> <ui-select-item value="all">All Statuses</ui-select-item> <ui-select-item value="active">Active</ui-select-item> <ui-select-item value="paused">Paused</ui-select-item> <ui-select-item value="error">Error</ui-select-item> </ui-select-content> </ui-select> <span ui-badge variant="outline" class="hidden h-8 items-center gap-1 font-mono text-xs md:inline-flex"> <lucide-icon [img]="Globe" class="size-3" /> UTC </span> </div> </div> </div> <div ui-card-content class="p-0"> <div class="overflow-x-auto"> <div ui-table> <thead ui-table-header> <tr ui-table-row class="hover:bg-transparent"> <th ui-table-head class="min-w-[240px]">Job Name & Target</th> <th ui-table-head class="min-w-[200px]">Schedule</th> <th ui-table-head class="w-20">Timezone</th> <th ui-table-head class="w-28">Status</th> <th ui-table-head class="min-w-[130px]">Next Run</th> <th ui-table-head class="min-w-[220px]">Last Execution</th> <th ui-table-head class="w-16 text-right">Actions</th> </tr> </thead> <tbody ui-table-body> @for (job of filteredJobs(); track job.id) { <tr ui-table-row class="group transition-colors"> <!-- Job Name & Target --> <td ui-table-cell class="py-3"> <div class="space-y-0.5"> <div class="flex items-center gap-1.5"> <span class="text-foreground text-sm font-medium">{{ job.name }}</span> @if (runningJobId() === job.id) { <lucide-icon [img]="RefreshCw" class="text-primary size-3.5 animate-spin" aria-label="Running now" /> } </div> <p class="text-muted-foreground max-w-[280px] truncate font-mono text-xs sm:max-w-[340px]" [title]="job.target" > {{ job.target }} </p> </div> </td> <!-- Cron Expression & Human Translation --> <td ui-table-cell class="py-3"> <div class="space-y-1"> <div class="flex items-center gap-1.5"> <code class="border-border bg-muted text-foreground rounded border px-1.5 py-0.5 font-mono text-xs font-semibold" >{{ job.cronExpression }}</code > </div> <p class="text-muted-foreground flex items-center gap-1 text-xs">{{ job.humanSchedule }}</p> </div> </td> <!-- Timezone --> <td ui-table-cell class="py-3"> <span class="text-muted-foreground font-mono text-xs font-medium">{{ job.timezone }}</span> </td> <!-- Status Badge --> <td ui-table-cell class="py-3"> @if (job.status === 'active') { <span ui-badge variant="success" class="gap-1 text-xs"> <span class="bg-success size-1.5 animate-pulse rounded-full"></span> Active </span> } @if (job.status === 'paused') { <span ui-badge variant="secondary" class="gap-1 text-xs"> <lucide-icon [img]="Pause" class="size-3" /> Paused </span> } @if (job.status === 'error') { <span ui-badge variant="destructive" class="gap-1 text-xs"> <lucide-icon [img]="AlertCircle" class="size-3" /> Error </span> } </td> <!-- Next Run --> <td ui-table-cell class="py-3"> <div class="flex items-center gap-1.5 text-xs"> <lucide-icon [img]="Clock" class="text-muted-foreground size-3.5 shrink-0" /> <span [class]="job.status === 'paused' ? 'text-muted-foreground' : 'text-foreground font-medium'" > {{ job.nextRun }} </span> </div> </td> <!-- Last Execution --> <td ui-table-cell class="py-3"> <div class="space-y-0.5"> <div class="flex items-center gap-1.5"> @if (job.lastExecution.status === 'success') { <lucide-icon [img]="CheckCircle2" class="text-success size-3.5 shrink-0" /> } @else { <lucide-icon [img]="XCircle" class="text-destructive size-3.5 shrink-0" /> } <span [class]="execCodeClass(job)">{{ job.lastExecution.code }}</span> <span class="text-muted-foreground font-mono text-xs" >· {{ job.lastExecution.duration }}</span > </div> <p class="text-muted-foreground text-xs">{{ job.lastExecution.timestamp }}</p> </div> </td> <!-- Action Menu --> <td ui-table-cell class="py-3 text-right"> <ui-dropdown-menu> <button ui-button ui-dropdown-menu-trigger variant="ghost" size="icon" class="size-8"> <lucide-icon [img]="MoreHorizontal" class="size-4" /> <span class="sr-only">Open actions</span> </button> <ui-dropdown-menu-content align="end" class="w-48"> <ui-dropdown-menu-label>Job Actions</ui-dropdown-menu-label> <ui-dropdown-menu-separator /> <ui-dropdown-menu-item (select)="triggerRun(job)"> <lucide-icon [img]="Play" class="text-success mr-2 size-4" /> Run Now </ui-dropdown-menu-item> <ui-dropdown-menu-item (select)="togglePause(job)"> @if (job.status === 'active') { <lucide-icon [img]="Pause" class="mr-2 size-4" /> Pause Schedule } @else { <lucide-icon [img]="Play" class="mr-2 size-4" /> Resume Schedule } </ui-dropdown-menu-item> <ui-dropdown-menu-item (select)="openLogs(job)"> <lucide-icon [img]="Terminal" class="mr-2 size-4" /> View Logs </ui-dropdown-menu-item> <ui-dropdown-menu-item (select)="applyPreset(job.cronExpression)"> <lucide-icon [img]="Pencil" class="mr-2 size-4" /> Inspect in Helper </ui-dropdown-menu-item> <ui-dropdown-menu-separator /> <ui-dropdown-menu-item class="text-destructive focus:text-destructive" (select)="deleteJob(job.id)" > <lucide-icon [img]="Trash2" class="mr-2 size-4" /> Delete Job </ui-dropdown-menu-item> </ui-dropdown-menu-content> </ui-dropdown-menu> </td> </tr> } @if (filteredJobs().length === 0) { <tr ui-table-row> <td ui-table-cell colspan="7" class="text-muted-foreground h-28 text-center text-sm"> No scheduled jobs found matching the active filter. </td> </tr> } </tbody> </div> </div> </div> </div> <!-- Cron Expression Helper Card --> <div ui-card class="shadow-xs"> <div ui-card-header> <div class="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between"> <div class="space-y-1"> <div class="flex items-center gap-2"> <lucide-icon [img]="Terminal" class="text-primary size-4" /> <h3 ui-card-title class="text-lg font-semibold">Interactive Cron Expression Helper</h3> </div> <p ui-card-description class="text-xs"> Generate standard 5-part cron syntax with real-time translation and upcoming execution previews. </p> </div> <!-- Quick Presets --> <div class="flex flex-wrap items-center gap-1.5 pt-2 sm:pt-0"> <span class="text-muted-foreground text-xs">Presets:</span> @for (preset of presets; track preset) { <button ui-button variant="outline" size="sm" class="h-7 font-mono text-xs" (click)="applyPreset(preset)" > {{ preset }} </button> } </div> </div> </div> <div ui-card-content class="space-y-6"> <!-- Frequency Selector Buttons --> <div class="flex flex-wrap gap-2"> @for (freq of frequencies; track freq) { <button ui-button [variant]="helperFrequency() === freq ? 'default' : 'outline'" size="sm" class="text-xs capitalize" (click)="helperFrequency.set(freq)" > Every {{ freq }} </button> } </div> <!-- Dynamic Frequency Inputs Grid --> <div class="border-border bg-muted/40 grid grid-cols-1 gap-4 rounded-lg border p-4 sm:grid-cols-2 lg:grid-cols-3" > @switch (helperFrequency()) { @case ('minute') { <div class="space-y-2"> <label class="text-foreground text-xs font-medium">Interval Interval</label> <ui-select [value]="helperMinuteInterval()" (valueChange)="helperMinuteInterval.set($event)"> <button ui-select-trigger class="bg-card h-9 w-full text-xs"> <ui-select-value placeholder="Select interval" /> </button> <ui-select-content> <ui-select-item value="1">Every 1 minute (* * * * *)</ui-select-item> <ui-select-item value="5">Every 5 minutes (*/5 * * * *)</ui-select-item> <ui-select-item value="10">Every 10 minutes (*/10 * * * *)</ui-select-item> <ui-select-item value="15">Every 15 minutes (*/15 * * * *)</ui-select-item> <ui-select-item value="30">Every 30 minutes (*/30 * * * *)</ui-select-item> </ui-select-content> </ui-select> </div> } @case ('hour') { <div class="space-y-2"> <label class="text-foreground text-xs font-medium">Minute of the Hour</label> <ui-select [value]="helperHourlyMinute()" (valueChange)="helperHourlyMinute.set($event)"> <button ui-select-trigger class="bg-card h-9 w-full text-xs"> <ui-select-value placeholder="Select minute" /> </button> <ui-select-content> <ui-select-item value="0">At minute :00 (Top of hour)</ui-select-item> <ui-select-item value="15">At minute :15 (Quarter past)</ui-select-item> <ui-select-item value="30">At minute :30 (Half past)</ui-select-item> <ui-select-item value="45">At minute :45 (Quarter to)</ui-select-item> </ui-select-content> </ui-select> </div> } @case ('day') { <div class="space-y-2"> <label class="text-foreground text-xs font-medium">Execution Hour (UTC)</label> <ui-select [value]="helperDailyHour()" (valueChange)="helperDailyHour.set($event)"> <button ui-select-trigger class="bg-card h-9 w-full text-xs"> <ui-select-value placeholder="Select hour" /> </button> <ui-select-content> <ui-select-item value="0">00:00 (Midnight)</ui-select-item> <ui-select-item value="4">04:00 (Early Morning)</ui-select-item> <ui-select-item value="9">09:00 (Morning)</ui-select-item> <ui-select-item value="12">12:00 (Noon)</ui-select-item> <ui-select-item value="18">18:00 (Evening)</ui-select-item> <ui-select-item value="23">23:00 (Late Night)</ui-select-item> </ui-select-content> </ui-select> </div> <div class="space-y-2"> <label class="text-foreground text-xs font-medium">Minute (UTC)</label> <ui-select [value]="helperDailyMinute()" (valueChange)="helperDailyMinute.set($event)"> <button ui-select-trigger class="bg-card h-9 w-full text-xs"> <ui-select-value placeholder="Select minute" /> </button> <ui-select-content> <ui-select-item value="0">:00</ui-select-item> <ui-select-item value="15">:15</ui-select-item> <ui-select-item value="30">:30</ui-select-item> <ui-select-item value="45">:45</ui-select-item> </ui-select-content> </ui-select> </div> } @case ('week') { <div class="space-y-2"> <label class="text-foreground text-xs font-medium">Day of Week</label> <ui-select [value]="helperWeekDay()" (valueChange)="helperWeekDay.set($event)"> <button ui-select-trigger class="bg-card h-9 w-full text-xs"> <ui-select-value placeholder="Select day" /> </button> <ui-select-content> <ui-select-item value="1">Monday (1)</ui-select-item> <ui-select-item value="2">Tuesday (2)</ui-select-item> <ui-select-item value="3">Wednesday (3)</ui-select-item> <ui-select-item value="4">Thursday (4)</ui-select-item> <ui-select-item value="5">Friday (5)</ui-select-item> <ui-select-item value="6">Saturday (6)</ui-select-item> <ui-select-item value="0">Sunday (0)</ui-select-item> </ui-select-content> </ui-select> </div> <div class="space-y-2"> <label class="text-foreground text-xs font-medium">Time (UTC)</label> <ui-select [value]="helperWeekHour()" (valueChange)="helperWeekHour.set($event)"> <button ui-select-trigger class="bg-card h-9 w-full text-xs"> <ui-select-value placeholder="Select hour" /> </button> <ui-select-content> <ui-select-item value="0">00:00 (Midnight)</ui-select-item> <ui-select-item value="9">09:00 (Morning)</ui-select-item> <ui-select-item value="12">12:00 (Noon)</ui-select-item> <ui-select-item value="18">18:00 (Evening)</ui-select-item> </ui-select-content> </ui-select> </div> } @case ('month') { <div class="space-y-2"> <label class="text-foreground text-xs font-medium">Day of Month</label> <ui-select [value]="helperMonthDay()" (valueChange)="helperMonthDay.set($event)"> <button ui-select-trigger class="bg-card h-9 w-full text-xs"> <ui-select-value placeholder="Select day of month" /> </button> <ui-select-content> <ui-select-item value="1">1st of the month</ui-select-item> <ui-select-item value="15">15th (Mid-month)</ui-select-item> <ui-select-item value="28">28th of the month</ui-select-item> </ui-select-content> </ui-select> </div> <div class="space-y-2"> <label class="text-foreground text-xs font-medium">Time (UTC)</label> <ui-select [value]="helperMonthHour()" (valueChange)="helperMonthHour.set($event)"> <button ui-select-trigger class="bg-card h-9 w-full text-xs"> <ui-select-value placeholder="Select hour" /> </button> <ui-select-content> <ui-select-item value="0">00:00 (Midnight)</ui-select-item> <ui-select-item value="9">09:00 (Morning)</ui-select-item> <ui-select-item value="12">12:00 (Noon)</ui-select-item> </ui-select-content> </ui-select> </div> } } <div class="flex flex-col justify-end space-y-2 sm:col-span-2 lg:col-span-1"> <label class="text-foreground text-xs font-medium">Target Cluster Timezone</label> <div class="border-border bg-card text-muted-foreground flex h-9 items-center gap-2 rounded-md border px-3 text-xs" > <lucide-icon [img]="Globe" class="text-primary size-3.5" /> <span>UTC (Coordinated Universal)</span> </div> </div> </div> <!-- Generated Syntax & Next 3 Runs Output --> <div class="grid grid-cols-1 gap-4 md:grid-cols-2"> <!-- Left: Expression and Humanized Label --> <div class="border-border bg-card space-y-3 rounded-lg border p-4"> <div class="flex items-center justify-between"> <span class="text-muted-foreground text-xs font-medium tracking-wider uppercase">Generated Syntax</span> <button ui-button variant="outline" size="sm" class="h-7 gap-1.5 text-xs" (click)="copyExpression(currentCronExpression())" > @if (copiedCron()) { <lucide-icon [img]="Check" class="text-success size-3" /> } @else { <lucide-icon [img]="Copy" class="size-3" /> } {{ copiedCron() ? 'Copied' : 'Copy Cron' }} </button> </div> <div class="border-border bg-muted/60 rounded-md border p-3"> <code class="text-foreground font-mono text-lg font-bold">{{ currentCronExpression() }}</code> </div> <div class="text-muted-foreground flex items-center gap-2 text-xs"> <lucide-icon [img]="CalendarClock" class="text-primary size-4 shrink-0" /> <span>{{ currentHumanTranslation() }}</span> </div> </div> <!-- Right: Next 3 Runs Preview --> <div class="border-border bg-card space-y-3 rounded-lg border p-4"> <div class="flex items-center justify-between"> <span class="text-muted-foreground text-xs font-medium tracking-wider uppercase"> Next 3 Scheduled Runs (UTC) </span> <span ui-badge variant="outline" class="font-mono text-xs">Simulated</span> </div> <div class="space-y-2"> @for (run of nextExecutionsPreview(); track $index) { <div class="border-border bg-muted/40 flex items-center justify-between rounded-md border px-3 py-2 text-xs" > <div class="flex items-center gap-2"> <span class="text-muted-foreground font-mono font-semibold">{{ $index + 1 }}.</span> <span class="text-foreground font-mono font-medium">{{ run.time }}</span> </div> <span ui-badge variant="secondary" class="text-xs font-normal">{{ run.relative }}</span> </div> } </div> </div> </div> </div> </div> <!-- Dialog: Create New Cron Job --> <ui-dialog [open]="isCreateOpen()" (openChange)="isCreateOpen.set($event)"> <ui-dialog-content class="sm:max-w-lg"> <div ui-dialog-header> <h2 ui-dialog-title>New Cron Job</h2> <p ui-dialog-description> Schedule a background worker function or HTTP endpoint target on a recurring cron interval. </p> </div> <div class="space-y-4 py-2"> <div class="space-y-1.5"> <label for="new-job-name-angular" class="text-foreground text-xs font-medium" >Job Name / Identifier</label > <ui-input id="new-job-name-angular" [value]="newJobName()" (valueChange)="newJobName.set($event)" placeholder="e.g. sync-stripe-disputes" /> </div> <div class="space-y-1.5"> <label for="new-job-target-angular" class="text-foreground text-xs font-medium"> Target Endpoint / Worker Handler </label> <ui-input id="new-job-target-angular" [value]="newJobTarget()" (valueChange)="newJobTarget.set($event)" placeholder="POST https://api.acme.corp/v1/stripe/disputes" /> </div> <div class="grid grid-cols-2 gap-3"> <div class="space-y-1.5"> <label for="new-job-cron-angular" class="text-foreground text-xs font-medium">Cron Expression</label> <ui-input id="new-job-cron-angular" [value]="newJobCron()" (valueChange)="newJobCron.set($event)" placeholder="0 4 * * *" class="font-mono text-xs" /> </div> <div class="space-y-1.5"> <label for="new-job-tz-angular" class="text-foreground text-xs font-medium">Timezone</label> <ui-select [value]="newJobTimezone()" (valueChange)="newJobTimezone.set($event)"> <button ui-select-trigger id="new-job-tz-angular" class="text-xs"> <ui-select-value placeholder="Timezone" /> </button> <ui-select-content> <ui-select-item value="UTC">UTC (Universal)</ui-select-item> <ui-select-item value="America/New_York">America/New_York (EST)</ui-select-item> <ui-select-item value="America/Los_Angeles">America/Los_Angeles (PST)</ui-select-item> <ui-select-item value="Europe/London">Europe/London (GMT)</ui-select-item> </ui-select-content> </ui-select> </div> </div> </div> <div ui-dialog-footer> <button ui-button variant="outline" (click)="isCreateOpen.set(false)">Cancel</button> <button ui-button [disabled]="!newJobName().trim()" (click)="handleCreateJob()">Create Cron Job</button> </div> </ui-dialog-content> </ui-dialog> <!-- Dialog: View Execution Logs --> <ui-dialog [open]="isLogsOpen()" (openChange)="isLogsOpen.set($event)"> <ui-dialog-content class="sm:max-w-2xl"> <div ui-dialog-header> <div class="flex items-center gap-2"> <lucide-icon [img]="Terminal" class="text-primary size-5" /> <h2 ui-dialog-title class="font-mono text-base">{{ selectedJobForLogs()?.name }}</h2> </div> <p ui-dialog-description class="font-mono text-xs"> Target: {{ selectedJobForLogs()?.target }} · Schedule: {{ selectedJobForLogs()?.cronExpression }} </p> </div> <div class="space-y-3 py-1"> <div class="border-border bg-muted/60 flex items-center justify-between rounded-md border px-3 py-2 text-xs" > <div class="flex items-center gap-2"> <span class="text-muted-foreground">Latest Run:</span> <span class="text-foreground font-mono font-medium"> {{ selectedJobForLogs()?.lastExecution?.timestamp }} </span> </div> <div class="flex items-center gap-2"> <span class="text-muted-foreground">Duration:</span> <span class="text-foreground font-mono">{{ selectedJobForLogs()?.lastExecution?.duration }}</span> </div> <span ui-badge [variant]="selectedJobForLogs()?.lastExecution?.status === 'success' ? 'success' : 'destructive'" class="font-mono text-xs" > {{ selectedJobForLogs()?.lastExecution?.code }} </span> </div> <!-- Terminal Output Simulation --> <div class="border-border text-success space-y-1.5 overflow-x-auto rounded-lg border bg-black/90 p-4 font-mono text-xs dark:bg-black" > <p class="text-muted-foreground"> ❯ [scheduler] Initializing invocation container for {{ selectedJobForLogs()?.name }}... </p> <p class="text-muted-foreground">❯ [network] Dispatching HTTP POST to target endpoint</p> <p class="text-success"> ✓ [worker] Request acknowledged with HTTP {{ selectedJobForLogs()?.lastExecution?.code }} </p> @if (selectedJobForLogs()?.lastExecution?.status === 'error') { <p class="text-destructive"> ✖ [error] Endpoint returned 504 Gateway Timeout after 30.0s threshold. Retrying with exponential backoff (attempt 1/3). </p> } @else { <p class="text-success"> ✓ [worker] Processed 1,420 queue records in {{ selectedJobForLogs()?.lastExecution?.duration }}. Exit status code: 0. </p> } <p class="text-muted-foreground"> ❯ [scheduler] Next scheduled execution: {{ selectedJobForLogs()?.nextRun }} </p> </div> </div> <div ui-dialog-footer> <button ui-button variant="outline" (click)="isLogsOpen.set(false)">Close Logs</button> </div> </ui-dialog-content> </ui-dialog> </div> `, }) export class UiCronJobSchedulerComponent { @Input() set initialJobs(v: CronJob[] | undefined) { this.jobs.set(v ?? defaultJobs) } @Input() set initialStats(v: CronStats | undefined) { this.stats = v ?? defaultStats } @Input('class') className?: string readonly Activity = Activity readonly AlertCircle = AlertCircle readonly AlertTriangle = AlertTriangle readonly CalendarClock = CalendarClock readonly Check = Check readonly CheckCircle2 = CheckCircle2 readonly Clock = Clock readonly Copy = Copy readonly Cpu = Cpu readonly Globe = Globe readonly MoreHorizontal = MoreHorizontal readonly Pause = Pause readonly Pencil = Pencil readonly Play = Play readonly Plus = Plus readonly RefreshCw = RefreshCw readonly Search = Search readonly Terminal = Terminal readonly Trash2 = Trash2 readonly X = X readonly XCircle = XCircle readonly Zap = Zap readonly presets = ['*/15 * * * *', '0 * * * *', '0 0 * * *', '0 9 * * 1', '0 0 1 * *'] as const readonly frequencies: CronFrequency[] = ['minute', 'hour', 'day', 'week', 'month'] stats: CronStats = defaultStats readonly jobs = signal<CronJob[]>(defaultJobs) readonly search = signal('') readonly statusFilter = signal<'all' | JobStatus>('all') readonly bannerMessage = signal<{ type: 'success' | 'info'; text: string } | null>(null) readonly runningJobId = signal<string | null>(null) // Dialogs readonly isCreateOpen = signal(false) readonly newJobName = signal('') readonly newJobTarget = signal('') readonly newJobCron = signal('0 0 * * *') readonly newJobTimezone = signal('UTC') readonly isLogsOpen = signal(false) readonly selectedJobForLogs = signal<CronJob | null>(null) // Helper state readonly helperFrequency = signal<CronFrequency>('day') readonly helperMinuteInterval = signal('15') readonly helperHourlyMinute = signal('0') readonly helperDailyHour = signal('0') readonly helperDailyMinute = signal('0') readonly helperWeekDay = signal('1') readonly helperWeekHour = signal('9') readonly helperWeekMinute = signal('0') readonly helperMonthDay = signal('1') readonly helperMonthHour = signal('0') readonly helperMonthMinute = signal('0') readonly copiedCron = signal(false) private readonly timers = new Set<ReturnType<typeof setTimeout>>() constructor() { inject(DestroyRef).onDestroy(() => this.timers.forEach((t) => clearTimeout(t))) } get rootClass(): string { return cn('w-full space-y-6', this.className) } readonly currentCronExpression = computed(() => { switch (this.helperFrequency()) { case 'minute': { const n = this.helperMinuteInterval() return n === '1' ? '* * * * *' : `*/${n} * * * *` } case 'hour': return `${this.helperHourlyMinute()} * * * *` case 'day': return `${this.helperDailyMinute()} ${this.helperDailyHour()} * * *` case 'week': return `${this.helperWeekMinute()} ${this.helperWeekHour()} * * ${this.helperWeekDay()}` case 'month': return `${this.helperMonthMinute()} ${this.helperMonthHour()} ${this.helperMonthDay()} * *` default: return '0 0 * * *' } }) readonly currentHumanTranslation = computed(() => { switch (this.helperFrequency()) { case 'minute': { const n = this.helperMinuteInterval() return n === '1' ? 'Runs every minute' : `Runs every ${n} minutes` } case 'hour': { const m = this.helperHourlyMinute() return m === '0' ? 'Runs at the start of every hour (minute 0)' : `Runs at minute ${pad(m)} of every hour` } case 'day': return `Runs every day at ${pad(this.helperDailyHour())}:${pad(this.helperDailyMinute())} UTC` case 'week': { const day = weekDayNames[this.helperWeekDay()] || 'Monday' return `Runs every ${day} at ${pad(this.helperWeekHour())}:${pad(this.helperWeekMinute())} UTC` } case 'month': return `Runs on day ${this.helperMonthDay()} of every month at ${pad(this.helperMonthHour())}:${pad(this.helperMonthMinute())} UTC` default: return 'Every day at midnight' } }) readonly nextExecutionsPreview = computed<{ time: string; relative: string }[]>(() => { const freq = this.helperFrequency() if (freq === 'minute') { const step = Number(this.helperMinuteInterval()) || 1 return [ { time: `2026-10-24 14:${String(step).padStart(2, '0')}:00 UTC`, relative: `in ${step} minutes` }, { time: `2026-10-24 14:${String(step * 2).padStart(2, '0')}:00 UTC`, relative: `in ${step * 2} minutes` }, { time: `2026-10-24 14:${String(step * 3).padStart(2, '0')}:00 UTC`, relative: `in ${step * 3} minutes` }, ] } if (freq === 'hour') { const min = pad(this.helperHourlyMinute()) return [ { time: `2026-10-24 15:${min}:00 UTC`, relative: 'in 42 minutes' }, { time: `2026-10-24 16:${min}:00 UTC`, relative: 'in 1 hour 42 min' }, { time: `2026-10-24 17:${min}:00 UTC`, relative: 'in 2 hours 42 min' }, ] } if (freq === 'day') { const hr = pad(this.helperDailyHour()) const min = pad(this.helperDailyMinute()) return [ { time: `2026-10-25 ${hr}:${min}:00 UTC`, relative: 'tomorrow' }, { time: `2026-10-26 ${hr}:${min}:00 UTC`, relative: 'in 2 days' }, { time: `2026-10-27 ${hr}:${min}:00 UTC`, relative: 'in 3 days' }, ] } if (freq === 'week') { const hr = pad(this.helperWeekHour()) const min = pad(this.helperWeekMinute()) return [ { time: `2026-10-26 ${hr}:${min}:00 UTC`, relative: 'next Monday' }, { time: `2026-11-02 ${hr}:${min}:00 UTC`, relative: 'in 9 days' }, { time: `2026-11-09 ${hr}:${min}:00 UTC`, relative: 'in 16 days' }, ] } const dom = pad(this.helperMonthDay()) const hr = pad(this.helperMonthHour()) const min = pad(this.helperMonthMinute()) return [ { time: `2026-11-${dom} ${hr}:${min}:00 UTC`, relative: 'in 8 days' }, { time: `2026-12-${dom} ${hr}:${min}:00 UTC`, relative: 'in 38 days' }, { time: `2027-01-${dom} ${hr}:${min}:00 UTC`, relative: 'in 69 days' }, ] }) readonly filteredJobs = computed(() => { const q = this.search().trim().toLowerCase() const status = this.statusFilter() return this.jobs().filter((job) => { const matchesStatus = status === 'all' || job.status === status const matchesSearch = !q || job.name.toLowerCase().includes(q) || job.target.toLowerCase().includes(q) return matchesStatus && matchesSearch }) }) execCodeClass(job: CronJob): string { return cn( 'font-mono text-xs font-medium', job.lastExecution.status === 'success' ? 'text-success' : 'text-destructive', ) } setStatusFilter(value: string): void { this.statusFilter.set(value as 'all' | JobStatus) } private later(fn: () => void, ms: number): void { const t = setTimeout(() => { this.timers.delete(t) fn() }, ms) this.timers.add(t) } applyPreset(cron: string): void { if (cron === '*/15 * * * *') { this.helperFrequency.set('minute') this.helperMinuteInterval.set('15') } else if (cron === '0 * * * *') { this.helperFrequency.set('hour') this.helperHourlyMinute.set('0') } else if (cron === '0 0 * * *') { this.helperFrequency.set('day') this.helperDailyHour.set('0') this.helperDailyMinute.set('0') } else if (cron === '0 9 * * 1') { this.helperFrequency.set('week') this.helperWeekDay.set('1') this.helperWeekHour.set('9') this.helperWeekMinute.set('0') } else if (cron === '0 0 1 * *') { this.helperFrequency.set('month') this.helperMonthDay.set('1') this.helperMonthHour.set('0') this.helperMonthMinute.set('0') } } async copyExpression(text: string): Promise<void> { try { await navigator.clipboard.writeText(text) } catch { // Clipboard can be unavailable (insecure context); still show the confirmation, like React. } this.copiedCron.set(true) this.later(() => this.copiedCron.set(false), 2000) } triggerRun(job: CronJob): void { this.runningJobId.set(job.id) this.bannerMessage.set({ type: 'success', text: `Job "${job.name}" queued and running now on cluster edge-worker-01.`, }) this.later(() => { this.runningJobId.set(null) this.jobs.update((prev) => prev.map((j) => j.id === job.id ? { ...j, lastExecution: { status: 'success', code: '200 OK', duration: '0.8s', timestamp: 'Just now' } } : j, ), ) }, 1200) } togglePause(job: CronJob): void { const nextStatus: JobStatus = job.status === 'active' ? 'paused' : 'active' this.jobs.update((prev) => prev.map((j) => j.id === job.id ? { ...j, status: nextStatus, nextRun: nextStatus === 'paused' ? 'Paused' : 'in 15 min' } : j, ), ) this.bannerMessage.set({ type: 'info', text: `Job "${job.name}" has been ${nextStatus === 'paused' ? 'paused' : 'resumed'}.`, }) } deleteJob(id: string): void { const target = this.jobs().find((j) => j.id === id) this.jobs.update((prev) => prev.filter((j) => j.id !== id)) this.bannerMessage.set({ type: 'info', text: `Job "${target?.name || id}" removed from scheduler queue.`, }) } openLogs(job: CronJob): void { this.selectedJobForLogs.set(job) this.isLogsOpen.set(true) } handleCreateJob(): void { const name = this.newJobName().trim() if (!name) return const newJob: CronJob = { id: `job-${Date.now()}`, name: name.toLowerCase().replace(/\s+/g, '-'), target: this.newJobTarget().trim() || 'POST https://api.acme.corp/v1/worker/execute', cronExpression: this.newJobCron().trim() || '0 0 * * *', humanSchedule: 'Custom scheduled trigger', timezone: this.newJobTimezone() || 'UTC', status: 'active', nextRun: 'in 5 min', lastExecution: { status: 'success', code: 'Pending initial run', duration: '—', timestamp: 'Never', }, } this.jobs.update((prev) => [newJob, ...prev]) this.isCreateOpen.set(false) this.newJobName.set('') this.newJobTarget.set('') this.bannerMessage.set({ type: 'success', text: `New cron job "${newJob.name}" successfully created and registered in cluster.`, }) } }src/app/components/blocks/cron-job-scheduler/index.ts0.2 kB
export { UiCronJobSchedulerComponent, type CronFrequency, type CronJob, type CronJobSchedulerProps, type CronStats, type JobStatus, type LastExecution, } from './cron-job-scheduler.component'
Raw manifest:https://uipkge.dev/r/angular/cron-job-scheduler.json