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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
export { Cron } from './cron'; export { Cron } from "./cron.js";
export { CronBuilder } from './cron-builder'; export { CronBuilder } from "./cron-builder.js";
export { CronDisplay } from './cron-display'; export { CronDisplay } from "./cron-display.js";
export { CronExample } from './cron-example'; export { CronExample } from "./cron-example.js";
export { CronInput } from './cron-input'; export { CronInput } from "./cron-input.js";
export { export {
type CronBuilderProps, type CronBuilderProps,
@ -17,4 +17,4 @@ export {
type CronProps, type CronProps,
type CronValidationResult, type CronValidationResult,
type PeriodConfig, 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 { export class IntlService {
document = inject(DOCUMENT); document = inject(DOCUMENT);
get Intl(): typeof Intl {
return this.document.defaultView?.Intl as typeof Intl;
}
get timezone() { get timezone() {
return Intl.DateTimeFormat().resolvedOptions().timeZone; return this.Intl.DateTimeFormat().resolvedOptions().timeZone;
} }
formatTimestamp(timestamp: number, options?: Intl.DateTimeFormatOptions) { formatTimestamp(timestamp: number, options?: Intl.DateTimeFormatOptions) {
@ -20,7 +24,7 @@ export class IntlService {
...options, ...options,
}; };
return new Intl.DateTimeFormat( return new this.Intl.DateTimeFormat(
this.document.defaultView?.navigator.language, this.document.defaultView?.navigator.language,
{ {
...defaultOptions, ...defaultOptions,

View File

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

View File

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

View File

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