This commit is contained in:
master 2025-07-08 00:54:34 +08:00
parent 5be5b9f634
commit 81bf27ed28
15 changed files with 540 additions and 2928 deletions

38
Cargo.lock generated
View File

@ -551,7 +551,7 @@ dependencies = [
"derive_builder",
"diligent-date-parser",
"never",
"quick-xml",
"quick-xml 0.37.5",
"serde",
]
@ -1922,6 +1922,17 @@ dependencies = [
"serde",
]
[[package]]
name = "derivative"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "derive_builder"
version = "0.20.2"
@ -2332,11 +2343,12 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fancy-regex"
version = "0.14.0"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298"
checksum = "d6215aee357f8c7c989ebb4b8466ca4d7dc93b3957039f2fc3ea2ade8ea5f279"
dependencies = [
"bit-set",
"derivative",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
]
@ -4394,7 +4406,7 @@ dependencies = [
"futures",
"httparse",
"network-interface",
"quick-xml",
"quick-xml 0.37.5",
"reqwest",
"serde",
"tokio",
@ -5166,7 +5178,7 @@ dependencies = [
"itertools 0.14.0",
"parking_lot 0.12.4",
"percent-encoding",
"quick-xml",
"quick-xml 0.37.5",
"rand 0.9.1",
"reqwest",
"ring",
@ -5219,7 +5231,7 @@ dependencies = [
"log",
"md-5",
"percent-encoding",
"quick-xml",
"quick-xml 0.37.5",
"reqwest",
"serde",
"serde_json",
@ -6505,6 +6517,16 @@ dependencies = [
"serde",
]
[[package]]
name = "quick-xml"
version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8927b0664f5c5a98265138b7e3f90aa19a6b21353182469ace36d4ac527b7b1b"
dependencies = [
"memchr",
"serde",
]
[[package]]
name = "quinn"
version = "0.11.8"
@ -6798,7 +6820,7 @@ dependencies = [
"paste",
"percent-encoding",
"polars",
"quick-xml",
"quick-xml 0.38.0",
"quirks_path",
"rand 0.9.1",
"regex",
@ -7233,7 +7255,7 @@ dependencies = [
"atom_syndication",
"derive_builder",
"never",
"quick-xml",
"quick-xml 0.37.5",
"serde",
]

View File

@ -109,7 +109,7 @@ sea-orm = { version = "1.1", features = [
figment = { version = "0.10", features = ["toml", "json", "env", "yaml"] }
sea-orm-migration = { version = "1.1", features = ["runtime-tokio"] }
rss = { version = "2", features = ["builders", "with-serde"] }
fancy-regex = "0.14"
fancy-regex = "0.15"
lightningcss = "1.0.0-alpha.66"
html-escape = "0.2.13"
opendal = { version = "0.53", features = ["default", "services-fs"] }
@ -126,6 +126,7 @@ seaography = { version = "1.1", features = [
"with-postgres-array",
"with-json-as-scalar",
"with-custom-as-json",
"with-chrono-datetime-utc-as-timestamp",
] }
tower = { version = "0.5.2", features = ["util"] }
tower-http = { version = "0.6", features = [
@ -160,7 +161,7 @@ polars = { version = "0.49.1", features = [
"lazy",
"diagonal_concat",
], optional = true }
quick-xml = { version = "0.37.5", features = [
quick-xml = { version = "0.38", features = [
"serialize",
"serde-types",
"serde",

View File

@ -1,5 +1,5 @@
import { getFutureMatches } from '@datasert/cronjs-matcher';
import { Calendar, Clock, Info, Settings, Zap } from 'lucide-react';
import { getFutureMatches } from "@datasert/cronjs-matcher";
import { Calendar, Clock, Info, Settings, Zap } from "lucide-react";
import {
type CSSProperties,
type FC,
@ -8,109 +8,109 @@ import {
useEffect,
useMemo,
useState,
} from 'react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
} from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Separator } from '@/components/ui/separator';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
import { cn } from '@/presentation/utils';
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { cn } from "@/presentation/utils";
import {
type CronBuilderProps,
CronField,
type CronFieldConfig,
CronPeriod,
type CronPreset,
} from './types';
} from "./types.js";
const CRON_PRESETS: CronPreset[] = [
{
label: 'Every minute',
value: '0 * * * * *',
description: 'Runs every minute',
category: 'common',
label: "Every minute",
value: "0 * * * * *",
description: "Runs every minute",
category: "common",
},
{
label: 'Every 5 minutes',
value: '0 */5 * * * *',
description: 'Runs every 5 minutes',
category: 'common',
label: "Every 5 minutes",
value: "0 */5 * * * *",
description: "Runs every 5 minutes",
category: "common",
},
{
label: 'Every 15 minutes',
value: '0 */15 * * * *',
description: 'Runs every 15 minutes',
category: 'common',
label: "Every 15 minutes",
value: "0 */15 * * * *",
description: "Runs every 15 minutes",
category: "common",
},
{
label: 'Every 30 minutes',
value: '0 */30 * * * *',
description: 'Runs every 30 minutes',
category: 'common',
label: "Every 30 minutes",
value: "0 */30 * * * *",
description: "Runs every 30 minutes",
category: "common",
},
{
label: 'Every hour',
value: '0 0 * * * *',
description: 'Runs at the top of every hour',
category: 'common',
label: "Every hour",
value: "0 0 * * * *",
description: "Runs at the top of every hour",
category: "common",
},
{
label: 'Every 6 hours',
value: '0 0 */6 * * *',
description: 'Runs every 6 hours',
category: 'common',
label: "Every 6 hours",
value: "0 0 */6 * * *",
description: "Runs every 6 hours",
category: "common",
},
{
label: 'Daily at midnight',
value: '0 0 0 * * *',
description: 'Runs once daily at 00:00',
category: 'daily',
label: "Daily at midnight",
value: "0 0 0 * * *",
description: "Runs once daily at 00:00",
category: "daily",
},
{
label: 'Daily at 9 AM',
value: '0 0 9 * * *',
description: 'Runs daily at 9:00 AM',
category: 'daily',
label: "Daily at 9 AM",
value: "0 0 9 * * *",
description: "Runs daily at 9:00 AM",
category: "daily",
},
{
label: 'Weekdays at 9 AM',
value: '0 0 9 * * 1-5',
description: 'Runs Monday to Friday at 9:00 AM',
category: 'weekly',
label: "Weekdays at 9 AM",
value: "0 0 9 * * 1-5",
description: "Runs Monday to Friday at 9:00 AM",
category: "weekly",
},
{
label: 'Every Sunday',
value: '0 0 0 * * 0',
description: 'Runs every Sunday at midnight',
category: 'weekly',
label: "Every Sunday",
value: "0 0 0 * * 0",
description: "Runs every Sunday at midnight",
category: "weekly",
},
{
label: 'First day of month',
value: '0 0 0 1 * *',
description: 'Runs on the 1st day of every month',
category: 'monthly',
label: "First day of month",
value: "0 0 0 1 * *",
description: "Runs on the 1st day of every month",
category: "monthly",
},
{
label: 'Every year',
value: '0 0 0 1 1 *',
description: 'Runs on January 1st every year',
category: 'yearly',
label: "Every year",
value: "0 0 0 1 1 *",
description: "Runs on January 1st every year",
category: "yearly",
},
];
@ -119,98 +119,98 @@ const FIELD_CONFIGS: Record<CronField, CronFieldConfig> = {
min: 0,
max: 59,
step: 1,
allowSpecial: ['*', '?'],
allowSpecial: ["*", "?"],
},
minutes: {
min: 0,
max: 59,
step: 1,
allowSpecial: ['*', '?'],
allowSpecial: ["*", "?"],
},
hours: {
min: 0,
max: 23,
step: 1,
allowSpecial: ['*', '?'],
allowSpecial: ["*", "?"],
},
dayOfMonth: {
min: 1,
max: 31,
step: 1,
allowSpecial: ['*', '?', 'L', 'W'],
allowSpecial: ["*", "?", "L", "W"],
options: [
{ label: 'Any day', value: '*' },
{ label: 'No specific day', value: '?' },
{ label: 'Last day', value: 'L' },
{ label: 'Weekday', value: 'W' },
{ label: "Any day", value: "*" },
{ label: "No specific day", value: "?" },
{ label: "Last day", value: "L" },
{ label: "Weekday", value: "W" },
],
},
month: {
min: 1,
max: 12,
step: 1,
allowSpecial: ['*'],
allowSpecial: ["*"],
options: [
{ label: 'January', value: 1 },
{ label: 'February', value: 2 },
{ label: 'March', value: 3 },
{ label: 'April', value: 4 },
{ label: 'May', value: 5 },
{ label: 'June', value: 6 },
{ label: 'July', value: 7 },
{ label: 'August', value: 8 },
{ label: 'September', value: 9 },
{ label: 'October', value: 10 },
{ label: 'November', value: 11 },
{ label: 'December', value: 12 },
{ label: "January", value: 1 },
{ label: "February", value: 2 },
{ label: "March", value: 3 },
{ label: "April", value: 4 },
{ label: "May", value: 5 },
{ label: "June", value: 6 },
{ label: "July", value: 7 },
{ label: "August", value: 8 },
{ label: "September", value: 9 },
{ label: "October", value: 10 },
{ label: "November", value: 11 },
{ label: "December", value: 12 },
],
},
dayOfWeek: {
min: 0,
max: 6,
step: 1,
allowSpecial: ['*', '?'],
allowSpecial: ["*", "?"],
options: [
{ label: 'Sunday', value: 0 },
{ label: 'Monday', value: 1 },
{ label: 'Tuesday', value: 2 },
{ label: 'Wednesday', value: 3 },
{ label: 'Thursday', value: 4 },
{ label: 'Friday', value: 5 },
{ label: 'Saturday', value: 6 },
{ label: "Sunday", value: 0 },
{ label: "Monday", value: 1 },
{ label: "Tuesday", value: 2 },
{ label: "Wednesday", value: 3 },
{ label: "Thursday", value: 4 },
{ label: "Friday", value: 5 },
{ label: "Saturday", value: 6 },
],
},
year: {
min: 0,
max: 9999,
step: 1,
allowSpecial: ['*', '?'],
allowSpecial: ["*", "?"],
},
};
const PERIOD_CONFIGS = {
minute: {
label: CronPeriod.Minute,
description: 'Run every minute',
template: '0 * * * * *',
description: "Run every minute",
template: "0 * * * * *",
fields: [CronField.Minutes],
},
hourly: {
label: CronPeriod.Hourly,
description: 'Run every hour',
template: '0 0 * * * *',
description: "Run every hour",
template: "0 0 * * * *",
fields: [CronField.Minutes, CronField.Hours],
},
daily: {
label: CronPeriod.Daily,
description: 'Run every day',
template: '0 0 0 * * *',
description: "Run every day",
template: "0 0 0 * * *",
fields: [CronField.Seconds, CronField.Minutes, CronField.Hours],
},
weekly: {
label: CronPeriod.Weekly,
description: 'Run every week',
template: '0 0 0 * * 0',
description: "Run every week",
template: "0 0 0 * * 0",
fields: [
CronField.Seconds,
CronField.Minutes,
@ -220,8 +220,8 @@ const PERIOD_CONFIGS = {
},
monthly: {
label: CronPeriod.Monthly,
description: 'Run every month',
template: '0 0 0 1 * *',
description: "Run every month",
template: "0 0 0 1 * *",
fields: [
CronField.Seconds,
CronField.Minutes,
@ -231,8 +231,8 @@ const PERIOD_CONFIGS = {
},
yearly: {
label: CronPeriod.Yearly,
description: 'Run every year',
template: '0 0 0 1 1 *',
description: "Run every year",
template: "0 0 0 1 1 *",
fields: [
CronField.Seconds,
CronField.Minutes,
@ -243,8 +243,8 @@ const PERIOD_CONFIGS = {
},
custom: {
label: CronPeriod.Custom,
description: 'Custom expression',
template: '0 0 * * * *',
description: "Custom expression",
template: "0 0 * * * *",
fields: [
CronField.Seconds,
CronField.Minutes,
@ -257,8 +257,8 @@ const PERIOD_CONFIGS = {
} as const;
const CronBuilder: FC<CronBuilderProps> = ({
timezone = 'UTC',
value = '0 0 * * * *',
timezone = "UTC",
value = "0 0 * * * *",
onChange,
className,
disabled = false,
@ -301,7 +301,7 @@ const CronBuilder: FC<CronBuilderProps> = ({
});
return matches.map((match) => new Date(match));
} catch (error) {
console.error('Failed to get future matched runs', error);
console.error("Failed to get future matched runs", error);
return [];
}
}, [currentExpression, showPreview, timezone]);
@ -327,7 +327,7 @@ const CronBuilder: FC<CronBuilderProps> = ({
const handlePeriodChange = useCallback((period: CronPeriod) => {
setActiveTab(period);
if (period !== 'custom') {
if (period !== "custom") {
const config = PERIOD_CONFIGS[period];
setCronFields(parseCronExpression(config.template));
}
@ -335,7 +335,7 @@ const CronBuilder: FC<CronBuilderProps> = ({
const filteredPresets = useMemo(() => {
return presets.filter((preset) => {
if (activeTab === 'custom') {
if (activeTab === "custom") {
return true;
}
return preset.category === activeTab;
@ -343,7 +343,7 @@ const CronBuilder: FC<CronBuilderProps> = ({
}, [presets, activeTab]);
return (
<div className={cn(withCard && 'space-y-6', className)}>
<div className={cn(withCard && "space-y-6", className)}>
<Tabs
value={activeTab}
onValueChange={(v) => handlePeriodChange(v as CronPeriod)}
@ -353,11 +353,11 @@ const CronBuilder: FC<CronBuilderProps> = ({
className="grid w-(--all-grids-width) grid-cols-7 whitespace-nowrap lg:w-full"
style={
{
'--my-grid-cols': `grid-template-columns: repeat(${displayPeriods.length}, minmax(0, 1fr))`,
'--all-grids-width':
"--my-grid-cols": `grid-template-columns: repeat(${displayPeriods.length}, minmax(0, 1fr))`,
"--all-grids-width":
displayPeriods.length > 4
? `${displayPeriods.length * 25 - 20}%`
: '100%',
: "100%",
} as CSSProperties
}
>
@ -377,10 +377,10 @@ const CronBuilder: FC<CronBuilderProps> = ({
<TabsContent
key={period}
value={period}
className={cn(withCard ? 'space-y-4' : 'px-0')}
className={cn(withCard ? "space-y-4" : "px-0")}
>
<Card className={cn(!withCard && 'border-none shadow-none')}>
<CardHeader className={cn('pb-1', !withCard && 'px-0')}>
<Card className={cn(!withCard && "border-none shadow-none")}>
<CardHeader className={cn("pb-1", !withCard && "px-0")}>
<CardTitle className="flex items-center gap-2 text-base">
<Settings className="h-4 w-4" />
<span className="capitalize">
@ -391,7 +391,7 @@ const CronBuilder: FC<CronBuilderProps> = ({
{PERIOD_CONFIGS[period].description}
</CardDescription>
</CardHeader>
<CardContent className={cn('space-y-4', !withCard && 'px-0')}>
<CardContent className={cn("space-y-4", !withCard && "px-0")}>
<CronFieldEditor
period={period}
fields={cronFields}
@ -402,8 +402,8 @@ const CronBuilder: FC<CronBuilderProps> = ({
</Card>
{showPresets && filteredPresets.length > 0 && (
<Card className={cn(!withCard && 'border-none shadow-none')}>
<CardHeader className={cn(!withCard && 'px-0')}>
<Card className={cn(!withCard && "border-none shadow-none")}>
<CardHeader className={cn(!withCard && "px-0")}>
<CardTitle className="flex items-center gap-2 text-base">
<Zap className="h-4 w-4" />
Quick Presets
@ -412,7 +412,7 @@ const CronBuilder: FC<CronBuilderProps> = ({
Common cron expressions for quick setup
</CardDescription>
</CardHeader>
<CardContent className={cn(!withCard && 'px-0')}>
<CardContent className={cn(!withCard && "px-0")}>
<div className="grid gap-3 sm:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3">
{filteredPresets.map((preset, index) => (
<Button
@ -447,14 +447,14 @@ const CronBuilder: FC<CronBuilderProps> = ({
</Tabs>
{/* Current Expression & Preview */}
{showGeneratedExpression && (
<Card className={cn(!withCard && 'border-none shadow-none')}>
<CardHeader className={cn(!withCard && 'px-0')}>
<Card className={cn(!withCard && "border-none shadow-none")}>
<CardHeader className={cn(!withCard && "px-0")}>
<CardTitle className="flex items-center gap-2 text-base">
<Clock className="h-4 w-4" />
Generated Expression
</CardTitle>
</CardHeader>
<CardContent className={cn('space-y-4', !withCard && 'px-0')}>
<CardContent className={cn("space-y-4", !withCard && "px-0")}>
<div className="flex items-center gap-2">
<Badge variant="outline" className="px-3 py-1 font-mono text-sm">
{currentExpression}
@ -532,8 +532,8 @@ const CronFieldEditor: FC<CronFieldEditorProps> = ({
};
const CronFieldItemAnyOrSpecificOption = {
Any: 'any',
Specific: 'specific',
Any: "any",
Specific: "specific",
} as const;
type CronFieldItemAnyOrSpecificOption =
@ -548,11 +548,11 @@ interface CronFieldItemEditorProps {
}
function encodeCronFieldItem(value: string): string {
if (value === '') {
return '<meta:empty>';
if (value === "") {
return "<meta:empty>";
}
if (value.includes(' ')) {
if (value.includes(" ")) {
return `<meta:contains-space:${encodeURIComponent(value)}>`;
}
@ -560,15 +560,15 @@ function encodeCronFieldItem(value: string): string {
}
function decodeCronFieldItem(value: string): string {
if (value.startsWith('<meta:contains')) {
if (value.startsWith("<meta:contains")) {
return decodeURIComponent(
// biome-ignore lint/performance/useTopLevelRegex: false
value.replace(/^<meta:contains-space:([^>]+)>$/, '$1')
value.replace(/^<meta:contains-space:([^>]+)>$/, "$1")
);
}
if (value === '<meta:empty>') {
return '';
if (value === "<meta:empty>") {
return "";
}
return value;
@ -582,7 +582,7 @@ export const CronFieldItemEditor: FC<CronFieldItemEditorProps> = memo(
const [anyOrSpecificOption, _setAnyOrSpecificOption] =
useState<CronFieldItemAnyOrSpecificOption>(() =>
innerValue === '*'
innerValue === "*"
? CronFieldItemAnyOrSpecificOption.Any
: CronFieldItemAnyOrSpecificOption.Specific
);
@ -607,9 +607,9 @@ export const CronFieldItemEditor: FC<CronFieldItemEditorProps> = memo(
(v: CronFieldItemAnyOrSpecificOption) => {
_setAnyOrSpecificOption(v);
if (v === CronFieldItemAnyOrSpecificOption.Any) {
handleChange('*');
handleChange("*");
} else if (v === CronFieldItemAnyOrSpecificOption.Specific) {
handleChange('0');
handleChange("0");
}
},
[handleChange]
@ -618,10 +618,10 @@ export const CronFieldItemEditor: FC<CronFieldItemEditorProps> = memo(
return (
<div className="space-y-2">
<Label className="font-medium text-sm capitalize">
{field.replace(/([A-Z])/g, ' $1').toLowerCase()}
{field.replace(/([A-Z])/g, " $1").toLowerCase()}
</Label>
{(field === 'month' || field === 'dayOfWeek') && (
{(field === "month" || field === "dayOfWeek") && (
<Select
value={innerValue}
onValueChange={handleChange}
@ -640,7 +640,7 @@ export const CronFieldItemEditor: FC<CronFieldItemEditorProps> = memo(
</SelectContent>
</Select>
)}
{field === 'dayOfMonth' && (
{field === "dayOfMonth" && (
<div className="space-y-2">
<Select
value={innerValue}
@ -666,9 +666,9 @@ export const CronFieldItemEditor: FC<CronFieldItemEditorProps> = memo(
</div>
)}
{!(
field === 'month' ||
field === 'dayOfWeek' ||
field === 'dayOfMonth'
field === "month" ||
field === "dayOfWeek" ||
field === "dayOfMonth"
) && (
<div className="space-y-2">
<ToggleGroup
@ -722,21 +722,21 @@ export const CronFieldItemEditor: FC<CronFieldItemEditorProps> = memo(
);
function parseCronExpression(expression: string): Record<CronField, string> {
const parts = expression.split(' ');
const parts = expression.split(" ");
// Ensure we have 6 parts, pad with defaults if needed
while (parts.length < 6) {
parts.push('*');
parts.push("*");
}
return {
seconds: parts[0] || '0',
minutes: parts[1] || '*',
hours: parts[2] || '*',
dayOfMonth: parts[3] || '*',
month: parts[4] || '*',
dayOfWeek: parts[5] || '*',
year: parts[6] || '*',
seconds: parts[0] || "0",
minutes: parts[1] || "*",
hours: parts[2] || "*",
dayOfMonth: parts[3] || "*",
month: parts[4] || "*",
dayOfWeek: parts[5] || "*",
year: parts[6] || "*",
};
}

View File

@ -1,67 +1,67 @@
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Code2, Play, Settings, Type } from "lucide-react";
import { type FC, useCallback, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Code2, Play, Settings, Type } from 'lucide-react';
import { type FC, useCallback, useState } from 'react';
import { CronBuilder } from './cron-builder.js';
import { CronDisplay } from './cron-display.js';
import { CronInput } from './cron-input.js';
import { Cron } from './cron.js';
} from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Cron } from "./cron.jsx";
import { CronBuilder } from "./cron-builder.jsx";
import { CronDisplay } from "./cron-display.jsx";
import { CronInput } from "./cron-input.jsx";
const CronExample: FC = () => {
const [inputValue, setInputValue] = useState('0 30 9 * * 1-5');
const [builderValue, setBuilderValue] = useState('0 0 */6 * * *');
const [fullValue, setFullValue] = useState('0 */15 * * * *');
const [displayValue] = useState('0 0 0 * * 0');
const [inputValue, setInputValue] = useState("0 30 9 * * 1-5");
const [builderValue, setBuilderValue] = useState("0 0 */6 * * *");
const [fullValue, setFullValue] = useState("0 */15 * * * *");
const [displayValue] = useState("0 0 0 * * 0");
const examples = [
{
label: 'Every minute',
expression: '0 * * * * *',
description: 'Runs at the start of every minute',
label: "Every minute",
expression: "0 * * * * *",
description: "Runs at the start of every minute",
},
{
label: 'Every 5 minutes',
expression: '0 */5 * * * *',
description: 'Runs every 5 minutes',
label: "Every 5 minutes",
expression: "0 */5 * * * *",
description: "Runs every 5 minutes",
},
{
label: 'Every hour',
expression: '0 0 * * * *',
description: 'Runs at the start of every hour',
label: "Every hour",
expression: "0 0 * * * *",
description: "Runs at the start of every hour",
},
{
label: 'Daily at 9 AM',
expression: '0 0 9 * * *',
description: 'Runs every day at 9:00 AM',
label: "Daily at 9 AM",
expression: "0 0 9 * * *",
description: "Runs every day at 9:00 AM",
},
{
label: 'Weekdays at 9:30 AM',
expression: '0 30 9 * * 1-5',
description: 'Runs Monday through Friday at 9:30 AM',
label: "Weekdays at 9:30 AM",
expression: "0 30 9 * * 1-5",
description: "Runs Monday through Friday at 9:30 AM",
},
{
label: 'Every Sunday',
expression: '0 0 0 * * 0',
description: 'Runs every Sunday at midnight',
label: "Every Sunday",
expression: "0 0 0 * * 0",
description: "Runs every Sunday at midnight",
},
{
label: 'First day of month',
expression: '0 0 0 1 * *',
description: 'Runs on the 1st day of every month',
label: "First day of month",
expression: "0 0 0 1 * *",
description: "Runs on the 1st day of every month",
},
{
label: 'Every quarter',
expression: '0 0 0 1 */3 *',
description: 'Runs on the 1st day of every quarter',
label: "Every quarter",
expression: "0 0 0 1 */3 *",
description: "Runs on the 1st day of every quarter",
},
];
@ -69,7 +69,7 @@ const CronExample: FC = () => {
try {
await navigator.clipboard.writeText(expression);
} catch (error) {
console.warn('Failed to copy to clipboard:', error);
console.warn("Failed to copy to clipboard:", error);
}
}, []);
@ -165,7 +165,7 @@ const CronExample: FC = () => {
<div className="rounded bg-muted p-4">
<h4 className="mb-2 font-medium text-sm">Current Value:</h4>
<Badge variant="outline" className="font-mono">
{fullValue || 'No expression set'}
{fullValue || "No expression set"}
</Badge>
</div>
</div>
@ -196,7 +196,7 @@ const CronExample: FC = () => {
<div className="rounded bg-muted p-4">
<h4 className="mb-2 font-medium text-sm">Current Value:</h4>
<Badge variant="outline" className="font-mono">
{inputValue || 'No expression set'}
{inputValue || "No expression set"}
</Badge>
</div>
</div>
@ -244,7 +244,7 @@ const CronExample: FC = () => {
<div className="rounded bg-muted p-4">
<h4 className="mb-2 font-medium text-sm">Current Value:</h4>
<Badge variant="outline" className="font-mono">
{builderValue || 'No expression set'}
{builderValue || "No expression set"}
</Badge>
</div>
</div>

View File

@ -1,4 +1,4 @@
import { parse } from '@datasert/cronjs-parser';
import { parse } from "@datasert/cronjs-parser";
import {
AlertCircle,
Bolt,
@ -7,45 +7,45 @@ import {
Copy,
Settings,
Type,
} from 'lucide-react';
import { type FC, useCallback, useEffect, useMemo, useState } from 'react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
} from "lucide-react";
import { type FC, useCallback, useEffect, useMemo, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { cn } from '@/presentation/utils';
import { CronBuilder } from './cron-builder';
import { CronDisplay } from './cron-display';
import { CronInput } from './cron-input';
} from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/presentation/utils";
import { CronBuilder } from "./cron-builder.js";
import { CronDisplay } from "./cron-display.js";
import { CronInput } from "./cron-input.js";
import {
CronMode,
type CronPrimitiveMode,
type CronProps,
type CronValidationResult,
} from './types';
} from "./types.js";
const PLACEHOLDER = '0 0 * * * *';
const PLACEHOLDER = "0 0 * * * *";
const Cron: FC<CronProps> = ({
value = '',
value = "",
onChange,
activeMode = 'input',
activeMode = "input",
onActiveModeChange,
onValidate,
className,
mode = 'both',
mode = "both",
disabled = false,
placeholder = PLACEHOLDER,
showPreview = true,
showDescription = true,
timezone = 'UTC',
timezone = "UTC",
error,
children,
showHelp = true,
@ -57,7 +57,7 @@ const Cron: FC<CronProps> = ({
isFirstSibling = false,
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: false
}) => {
const [internalValue, setInternalValue] = useState(value || '');
const [internalValue, setInternalValue] = useState(value || "");
const [internalActiveMode, setInternalActiveMode] =
useState<CronPrimitiveMode>(
mode === CronMode.Both ? activeMode : (mode as CronPrimitiveMode)
@ -66,7 +66,7 @@ const Cron: FC<CronProps> = ({
const validationResult = useMemo((): CronValidationResult => {
if (!internalValue.trim()) {
return { isValid: false, error: 'Expression is required', isEmpty: true };
return { isValid: false, error: "Expression is required", isEmpty: true };
}
try {
@ -78,13 +78,13 @@ const Cron: FC<CronProps> = ({
error:
parseError instanceof Error
? parseError.message
: 'Invalid cron expression',
: "Invalid cron expression",
};
}
}, [internalValue]);
useEffect(() => {
setInternalValue(value || '');
setInternalValue(value || "");
}, [value]);
useEffect(() => {
@ -92,7 +92,7 @@ const Cron: FC<CronProps> = ({
}, [validationResult.isValid, onValidate]);
useEffect(() => {
if (mode === 'both') {
if (mode === "both") {
setInternalActiveMode(activeMode);
}
}, [activeMode, mode]);
@ -123,16 +123,16 @@ const Cron: FC<CronProps> = ({
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (e) {
console.warn('Failed to copy to clipboard:', e);
console.warn("Failed to copy to clipboard:", e);
}
}, [internalValue]);
const hasError =
!!error || !!(!validationResult.isValid && internalValue.trim());
if (mode === 'input') {
if (mode === "input") {
return (
<div className={cn(withCard && 'space-y-4', className)}>
<div className={cn(withCard && "space-y-4", className)}>
<CronInput
value={internalValue}
onChange={handleChange}
@ -161,9 +161,9 @@ const Cron: FC<CronProps> = ({
);
}
if (mode === 'builder') {
if (mode === "builder") {
return (
<div className={cn(withCard && 'space-y-4', className)}>
<div className={cn(withCard && "space-y-4", className)}>
<CronBuilder
value={internalValue}
onChange={handleChange}
@ -184,14 +184,14 @@ const Cron: FC<CronProps> = ({
}
return (
<div className={cn(withCard && 'space-y-6', className)}>
<div className={cn(withCard && "space-y-6", className)}>
<Card
className={cn(
!withCard && 'border-none shadow-none',
!withCard && isFirstSibling && 'pt-0'
!withCard && "border-none shadow-none",
!withCard && isFirstSibling && "pt-0"
)}
>
<CardHeader className={cn(!withCard && 'px-0')}>
<CardHeader className={cn(!withCard && "px-0")}>
<div className="flex items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2 text-base">
@ -207,7 +207,7 @@ const Cron: FC<CronProps> = ({
<div className="flex items-center gap-2">
<Badge
variant={
validationResult.isValid ? 'secondary' : 'destructive'
validationResult.isValid ? "secondary" : "destructive"
}
className="font-mono text-sm"
>
@ -238,11 +238,11 @@ const Cron: FC<CronProps> = ({
)}
</CardHeader>
<CardContent className={cn(!withCard && 'px-0')}>
<CardContent className={cn(!withCard && "px-0")}>
<Tabs
value={internalActiveMode}
onValueChange={(v) =>
handleActiveModeChange(v as 'input' | 'builder')
handleActiveModeChange(v as "input" | "builder")
}
>
<TabsList className="grid w-full grid-cols-2">
@ -314,14 +314,14 @@ const Cron: FC<CronProps> = ({
{showHelp && (
<>
{!withCard && <Separator />}
<Card className={cn(!withCard && 'border-none shadow-none')}>
<CardHeader className={cn(!withCard && 'px-0')}>
<Card className={cn(!withCard && "border-none shadow-none")}>
<CardHeader className={cn(!withCard && "px-0")}>
<CardTitle className="flex items-center gap-2 text-base">
<Code2 className="h-4 w-4" />
Cron Expression Format
</CardTitle>
</CardHeader>
<CardContent className={cn(!withCard && 'px-0')}>
<CardContent className={cn(!withCard && "px-0")}>
<div className="space-y-4">
<div className="grid grid-cols-6 gap-2 text-center text-sm">
<div className="space-y-1">

View File

@ -1,8 +1,8 @@
export { Cron } from './cron';
export { CronBuilder } from './cron-builder';
export { CronDisplay } from './cron-display';
export { CronExample } from './cron-example';
export { CronInput } from './cron-input';
export { Cron } from "./cron.js";
export { CronBuilder } from "./cron-builder.js";
export { CronDisplay } from "./cron-display.js";
export { CronExample } from "./cron-example.js";
export { CronInput } from "./cron-input.js";
export {
type CronBuilderProps,
@ -17,4 +17,4 @@ export {
type CronProps,
type CronValidationResult,
type PeriodConfig,
} from './types';
} from "./types.js";

File diff suppressed because it is too large Load Diff

View File

@ -4,8 +4,12 @@ import { DOCUMENT } from '../platform/injection';
export class IntlService {
document = inject(DOCUMENT);
get Intl(): typeof Intl {
return this.document.defaultView?.Intl as typeof Intl;
}
get timezone() {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
return this.Intl.DateTimeFormat().resolvedOptions().timeZone;
}
formatTimestamp(timestamp: number, options?: Intl.DateTimeFormatOptions) {
@ -20,7 +24,7 @@ export class IntlService {
...options,
};
return new Intl.DateTimeFormat(
return new this.Intl.DateTimeFormat(
this.document.defaultView?.navigator.language,
{
...defaultOptions,

View File

@ -1,5 +1,8 @@
import { Cron } from '@/components/domains/cron';
import { CronMode } from '@/components/domains/cron/types';
import { useMutation } from '@apollo/client';
import { useNavigate } from '@tanstack/react-router';
import { CalendarIcon } from 'lucide-react';
import { memo, useCallback, useState } from 'react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import {
Card,
@ -9,6 +12,8 @@ import {
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Cron } from '@/components/ui/cron';
import { CronMode } from '@/components/ui/cron/types';
import {
DialogContent,
DialogDescription,
@ -26,11 +31,6 @@ import {
SubscriberTaskTypeEnum,
} from '@/infra/graphql/gql/graphql';
import { IntlService } from '@/infra/intl/intl.service';
import { useMutation } from '@apollo/client';
import { useNavigate } from '@tanstack/react-router';
import { CalendarIcon } from 'lucide-react';
import { memo, useCallback, useState } from 'react';
import { toast } from 'sonner';
const SUBSCRIPTION_TASK_CRON_PRESETS = [
{
@ -162,59 +162,53 @@ export const SubscriptionCronCreationView = memo(
const loading = loadingInsert;
return (
<>
<Tabs
defaultValue={CRON_TABS[0].tab}
className="flex min-h-0 flex-1 flex-col"
>
<div className="flex w-full shrink-0 overflow-x-auto">
<TabsList className="flex items-center justify-center whitespace-nowrap">
{CRON_TABS.map((tab) => (
<TabsTrigger
key={tab.tab}
value={tab.tab}
className="w-fit px-4"
>
{tab.label}
</TabsTrigger>
))}
</TabsList>
</div>
{CRON_TABS.map((tab) => (
<TabsContent
key={tab.tab}
value={tab.tab}
className="flex-1 space-y-2"
asChild
>
<SubscriptionCronForm
tab={tab}
onComplete={(payload) => {
insertCron({
variables: {
data: {
cronExpr: payload.cronExpr,
cronTimezone: intlService.timezone,
subscriberTaskCron: {
subscriptionId,
taskType: tab.tab,
},
<Tabs
defaultValue={CRON_TABS[0].tab}
className="flex min-h-0 flex-1 flex-col"
>
<div className="flex w-full shrink-0 overflow-x-auto">
<TabsList className="flex items-center justify-center whitespace-nowrap">
{CRON_TABS.map((tab) => (
<TabsTrigger key={tab.tab} value={tab.tab} className="w-fit px-4">
{tab.label}
</TabsTrigger>
))}
</TabsList>
</div>
{CRON_TABS.map((tab) => (
<TabsContent
key={tab.tab}
value={tab.tab}
className="flex-1 space-y-2"
asChild
>
<SubscriptionCronForm
tab={tab}
onComplete={(payload) => {
insertCron({
variables: {
data: {
cronExpr: payload.cronExpr,
cronTimezone: intlService.timezone,
subscriberTaskCron: {
subscriptionId,
taskType: tab.tab,
},
},
});
}}
timezone={intlService.timezone}
/>
</TabsContent>
))}
{loading && (
<div className="absolute inset-0 flex flex-row items-center justify-center gap-2">
<Spinner variant="circle-filled" size="16" />
<span>Creating cron...</span>
</div>
)}
</Tabs>
</>
},
});
}}
timezone={intlService.timezone}
/>
</TabsContent>
))}
{loading && (
<div className="absolute inset-0 flex flex-row items-center justify-center gap-2">
<Spinner variant="circle-filled" size="16" />
<span>Creating cron...</span>
</div>
)}
</Tabs>
);
}
);

View File

@ -3,8 +3,6 @@ import { createFileRoute } from '@tanstack/react-router';
import { format } from 'date-fns';
import { RefreshCw } from 'lucide-react';
import { useMemo } from 'react';
import { CronDisplay } from '@/components/domains/cron';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
@ -15,17 +13,20 @@ import {
CardTitle,
} from '@/components/ui/card';
import { ContainerHeader } from '@/components/ui/container-header';
import { CronDisplay } from '@/components/ui/cron';
import { DetailCardSkeleton } from '@/components/ui/detail-card-skeleton';
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
import { Label } from '@/components/ui/label';
import { QueryErrorView } from '@/components/ui/query-error-view';
import { Separator } from '@/components/ui/separator';
import { GET_CRONS } from '@/domains/recorder/schema/cron';
import { useInject } from '@/infra/di/inject';
import {
CronStatusEnum,
type GetCronsQuery,
type GetCronsQueryVariables,
} from '@/infra/graphql/gql/graphql';
import { IntlService } from '@/infra/intl/intl.service';
import type { RouteStateDataOption } from '@/infra/routes/traits';
import { getStatusBadge } from './-status-badge';
@ -38,6 +39,7 @@ export const Route = createFileRoute('/_app/tasks/cron/detail/$id')({
function CronDetailRouteComponent() {
const { id } = Route.useParams();
const intlService = useInject(IntlService);
const { data, loading, error, refetch } = useQuery<
GetCronsQuery,
@ -115,7 +117,7 @@ function CronDetailRouteComponent() {
{/* Basic Information */}
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div className="space-y-2">
<Label className="font-medium text-sm">Cron ID</Label>
<Label className="font-medium text-sm">ID</Label>
<div className="rounded-md bg-muted p-3">
<code className="text-sm">{cron.id}</code>
</div>
@ -129,7 +131,7 @@ function CronDetailRouteComponent() {
</div>
<div className="space-y-2">
<Label className="font-medium text-sm">Retry count</Label>
<Label className="font-medium text-sm">Attemps</Label>
<div className="rounded-md bg-muted p-3">
<span className="text-sm">
{cron.attempts} / {cron.maxAttempts}
@ -199,7 +201,10 @@ function CronDetailRouteComponent() {
<Label className="font-medium text-sm">Created at</Label>
<div className="rounded-md bg-muted p-3">
<span className="text-sm">
{format(new Date(cron.createdAt), 'yyyy-MM-dd HH:mm:ss')}
{intlService.formatTimestamp(
cron.createdAt,
'yyyy-MM-dd HH:mm:ss'
)}
</span>
</div>
</div>

View File

@ -41,9 +41,13 @@
"noBannedTypes": "off"
},
"correctness": {
"noUnusedVariables": {
"fix": "none",
"level": "error"
},
"noUnusedImports": {
"fix": "none",
"level": "warn"
"level": "error"
}
}
}