Compare commits
3 Commits
6cdd8c27ce
...
94919878ea
Author | SHA1 | Date | |
---|---|---|---|
94919878ea | |||
81bf27ed28 | |||
5be5b9f634 |
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -41,4 +41,4 @@
|
||||
],
|
||||
"rust-analyzer.cargo.features": "all",
|
||||
"rust-analyzer.testExplorer": true
|
||||
}
|
||||
}
|
||||
|
52
Cargo.lock
generated
52
Cargo.lock
generated
@ -551,7 +551,7 @@ dependencies = [
|
||||
"derive_builder",
|
||||
"diligent-date-parser",
|
||||
"never",
|
||||
"quick-xml",
|
||||
"quick-xml 0.37.5",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@ -1260,9 +1260,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.40"
|
||||
version = "4.5.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f"
|
||||
checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@ -1270,9 +1270,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.40"
|
||||
version = "4.5.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e"
|
||||
checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@ -1282,9 +1282,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.40"
|
||||
version = "4.5.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce"
|
||||
checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
@ -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",
|
||||
]
|
||||
|
||||
@ -7660,7 +7682,7 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
[[package]]
|
||||
name = "seaography"
|
||||
version = "1.1.4"
|
||||
source = "git+https://github.com/dumtruck/seaography.git?rev=9f7fc7c#9f7fc7cf05234abe35fd9144c895321dd2b5db62"
|
||||
source = "git+https://github.com/dumtruck/seaography.git?rev=292cdd2#292cdd248217fdcf81c41aa97fe1c047c9b5f4de"
|
||||
dependencies = [
|
||||
"async-graphql",
|
||||
"fnv",
|
||||
|
22
Cargo.toml
22
Cargo.toml
@ -31,22 +31,22 @@ reqwest = { version = "0.12.20", features = [
|
||||
"macos-system-configuration",
|
||||
"cookies",
|
||||
] }
|
||||
moka = "0.12"
|
||||
futures = "0.3"
|
||||
quirks_path = "0.1"
|
||||
snafu = { version = "0.8", features = ["futures"] }
|
||||
testcontainers = { version = "0.24" }
|
||||
moka = "0.12.10"
|
||||
futures = "0.3.31"
|
||||
quirks_path = "0.1.1"
|
||||
snafu = { version = "0.8.0", features = ["futures"] }
|
||||
testcontainers = { version = "0.24.0" }
|
||||
testcontainers-modules = { version = "0.12.1" }
|
||||
testcontainers-ext = { version = "0.1.0", features = ["tracing"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
tokio = { version = "1.45.1", features = [
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
tokio = { version = "1.46", features = [
|
||||
"macros",
|
||||
"fs",
|
||||
"rt-multi-thread",
|
||||
"signal",
|
||||
] }
|
||||
serde_json = "1"
|
||||
async-trait = "0.1"
|
||||
serde_json = "1.0.140"
|
||||
async-trait = "0.1.88"
|
||||
tracing = "0.1"
|
||||
url = "2.5.2"
|
||||
anyhow = "1"
|
||||
@ -77,7 +77,7 @@ http = "1.2.0"
|
||||
async-stream = "0.3.6"
|
||||
serde_variant = "0.1.3"
|
||||
tracing-appender = "0.2.3"
|
||||
clap = "4.5.40"
|
||||
clap = "4.5.41"
|
||||
ipnetwork = "0.21.1"
|
||||
typed-builder = "0.21.0"
|
||||
nanoid = "0.4.0"
|
||||
@ -85,4 +85,4 @@ webp = "0.3.0"
|
||||
|
||||
|
||||
[patch.crates-io]
|
||||
seaography = { git = "https://github.com/dumtruck/seaography.git", rev = "9f7fc7c" }
|
||||
seaography = { git = "https://github.com/dumtruck/seaography.git", rev = "292cdd2" }
|
||||
|
@ -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"] }
|
||||
@ -160,7 +160,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",
|
||||
|
@ -12,6 +12,7 @@ const config: CodegenConfig = {
|
||||
},
|
||||
config: {
|
||||
enumsAsConst: true,
|
||||
useTypeImports: true,
|
||||
scalars: {
|
||||
SubscriberTaskType: {
|
||||
input: 'recorder/bindings/SubscriberTaskInput#SubscriberTaskInput',
|
||||
|
@ -1,4 +1,3 @@
|
||||
import type { NavMainGroup } from '@/infra/routes/nav';
|
||||
import {
|
||||
BookOpen,
|
||||
Folders,
|
||||
@ -9,6 +8,7 @@ import {
|
||||
Telescope,
|
||||
Tv,
|
||||
} from 'lucide-react';
|
||||
import type { NavMainGroup } from '@/infra/routes/nav';
|
||||
|
||||
export const AppNavMainData: NavMainGroup[] = [
|
||||
{
|
||||
@ -49,13 +49,13 @@ export const AppNavMainData: NavMainGroup[] = [
|
||||
{
|
||||
title: 'Manage',
|
||||
link: {
|
||||
to: '/bangumi/recorder',
|
||||
to: '/bangumi',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Feed',
|
||||
link: {
|
||||
to: '/bangumi/feed',
|
||||
to: '/bangumi',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -1,643 +0,0 @@
|
||||
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';
|
||||
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';
|
||||
import { getFutureMatches } from '@datasert/cronjs-matcher';
|
||||
import { Calendar, Clock, Info, Settings, Zap } from 'lucide-react';
|
||||
import {
|
||||
type CSSProperties,
|
||||
type FC,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {
|
||||
type CronBuilderProps,
|
||||
CronField,
|
||||
type CronFieldConfig,
|
||||
CronPeriod,
|
||||
type CronPreset,
|
||||
} from './types';
|
||||
|
||||
const CRON_PRESETS: CronPreset[] = [
|
||||
{
|
||||
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 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 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: '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: '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: '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',
|
||||
},
|
||||
];
|
||||
|
||||
const FIELD_CONFIGS: Record<CronField, CronFieldConfig> = {
|
||||
seconds: {
|
||||
min: 0,
|
||||
max: 59,
|
||||
step: 1,
|
||||
allowSpecial: ['*', '?'],
|
||||
},
|
||||
minutes: {
|
||||
min: 0,
|
||||
max: 59,
|
||||
step: 1,
|
||||
allowSpecial: ['*', '?'],
|
||||
},
|
||||
hours: {
|
||||
min: 0,
|
||||
max: 23,
|
||||
step: 1,
|
||||
allowSpecial: ['*', '?'],
|
||||
},
|
||||
dayOfMonth: {
|
||||
min: 1,
|
||||
max: 31,
|
||||
step: 1,
|
||||
allowSpecial: ['*', '?', 'L', 'W'],
|
||||
options: [
|
||||
{ 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: ['*'],
|
||||
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 },
|
||||
],
|
||||
},
|
||||
dayOfWeek: {
|
||||
min: 0,
|
||||
max: 6,
|
||||
step: 1,
|
||||
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 },
|
||||
],
|
||||
},
|
||||
year: {
|
||||
min: 0,
|
||||
max: 9999,
|
||||
step: 1,
|
||||
allowSpecial: ['*', '?'],
|
||||
},
|
||||
};
|
||||
|
||||
const PERIOD_CONFIGS = {
|
||||
minute: {
|
||||
label: CronPeriod.Minute,
|
||||
description: 'Run every minute',
|
||||
template: '0 * * * * *',
|
||||
fields: [CronField.Minutes],
|
||||
},
|
||||
hourly: {
|
||||
label: CronPeriod.Hourly,
|
||||
description: 'Run every hour',
|
||||
template: '0 0 * * * *',
|
||||
fields: [CronField.Minutes, CronField.Hours],
|
||||
},
|
||||
daily: {
|
||||
label: CronPeriod.Daily,
|
||||
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',
|
||||
fields: [
|
||||
CronField.Seconds,
|
||||
CronField.Minutes,
|
||||
CronField.Hours,
|
||||
CronField.DayOfWeek,
|
||||
],
|
||||
},
|
||||
monthly: {
|
||||
label: CronPeriod.Monthly,
|
||||
description: 'Run every month',
|
||||
template: '0 0 0 1 * *',
|
||||
fields: [
|
||||
CronField.Seconds,
|
||||
CronField.Minutes,
|
||||
CronField.Hours,
|
||||
CronField.DayOfMonth,
|
||||
],
|
||||
},
|
||||
yearly: {
|
||||
label: CronPeriod.Yearly,
|
||||
description: 'Run every year',
|
||||
template: '0 0 0 1 1 *',
|
||||
fields: [
|
||||
CronField.Seconds,
|
||||
CronField.Minutes,
|
||||
CronField.Hours,
|
||||
CronField.DayOfMonth,
|
||||
CronField.Month,
|
||||
],
|
||||
},
|
||||
custom: {
|
||||
label: CronPeriod.Custom,
|
||||
description: 'Custom expression',
|
||||
template: '0 0 * * * *',
|
||||
fields: [
|
||||
CronField.Seconds,
|
||||
CronField.Minutes,
|
||||
CronField.Hours,
|
||||
CronField.DayOfMonth,
|
||||
CronField.Month,
|
||||
CronField.DayOfWeek,
|
||||
],
|
||||
},
|
||||
} as const;
|
||||
|
||||
const CronBuilder: FC<CronBuilderProps> = ({
|
||||
timezone = 'UTC',
|
||||
value = '0 0 * * * *',
|
||||
onChange,
|
||||
className,
|
||||
disabled = false,
|
||||
showPreview = true,
|
||||
showPresets = true,
|
||||
displayPeriods = [
|
||||
CronPeriod.Custom,
|
||||
CronPeriod.Minute,
|
||||
CronPeriod.Hourly,
|
||||
CronPeriod.Daily,
|
||||
CronPeriod.Weekly,
|
||||
CronPeriod.Monthly,
|
||||
CronPeriod.Yearly,
|
||||
],
|
||||
defaultTab = CronPeriod.Custom,
|
||||
presets = CRON_PRESETS,
|
||||
showGeneratedExpression = true,
|
||||
withCard = true,
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState<CronPeriod>(defaultTab);
|
||||
const [cronFields, setCronFields] = useState(() =>
|
||||
parseCronExpression(value)
|
||||
);
|
||||
|
||||
const currentExpression = useMemo(() => {
|
||||
return `${cronFields.seconds} ${cronFields.minutes} ${cronFields.hours} ${cronFields.dayOfMonth} ${cronFields.month} ${cronFields.dayOfWeek}`;
|
||||
}, [cronFields]);
|
||||
|
||||
const nextRuns = useMemo(() => {
|
||||
if (!showPreview) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const matches = getFutureMatches(`${currentExpression} *`, {
|
||||
matchCount: 3,
|
||||
timezone,
|
||||
formatInTimezone: true,
|
||||
hasSeconds: true,
|
||||
});
|
||||
return matches.map((match) => new Date(match));
|
||||
} catch (error) {
|
||||
console.error('Failed to get future matched runs', error);
|
||||
return [];
|
||||
}
|
||||
}, [currentExpression, showPreview, timezone]);
|
||||
|
||||
useEffect(() => {
|
||||
setCronFields(parseCronExpression(value));
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
onChange?.(currentExpression);
|
||||
}, [currentExpression, onChange]);
|
||||
|
||||
const handlePresetSelect = useCallback((preset: CronPreset) => {
|
||||
setCronFields(parseCronExpression(preset.value));
|
||||
}, []);
|
||||
|
||||
const handleFieldChange = useCallback(
|
||||
(field: CronField, newValue: string) => {
|
||||
setCronFields((prev) => ({ ...prev, [field]: newValue }));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handlePeriodChange = useCallback((period: CronPeriod) => {
|
||||
setActiveTab(period);
|
||||
if (period !== 'custom') {
|
||||
const config = PERIOD_CONFIGS[period];
|
||||
setCronFields(parseCronExpression(config.template));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const filteredPresets = useMemo(() => {
|
||||
return presets.filter((preset) => {
|
||||
if (activeTab === 'custom') {
|
||||
return true;
|
||||
}
|
||||
return preset.category === activeTab;
|
||||
});
|
||||
}, [presets, activeTab]);
|
||||
|
||||
return (
|
||||
<div className={cn(withCard && 'space-y-6', className)}>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={(value) => handlePeriodChange(value as CronPeriod)}
|
||||
>
|
||||
<div className="overflow-x-auto">
|
||||
<TabsList
|
||||
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':
|
||||
displayPeriods.length > 4
|
||||
? `${displayPeriods.length * 25 - 20}%`
|
||||
: '100%',
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
{displayPeriods.map((period) => (
|
||||
<TabsTrigger
|
||||
key={period}
|
||||
value={period}
|
||||
disabled={disabled}
|
||||
className="text-xs capitalize"
|
||||
>
|
||||
{PERIOD_CONFIGS[period].label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</div>
|
||||
{displayPeriods.map((period) => (
|
||||
<TabsContent
|
||||
key={period}
|
||||
value={period}
|
||||
className={cn(withCard ? 'space-y-4' : '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">
|
||||
{PERIOD_CONFIGS[period].label} Configuration
|
||||
</span>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{PERIOD_CONFIGS[period].description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className={cn('space-y-4', !withCard && 'px-0')}>
|
||||
<CronFieldEditor
|
||||
period={period}
|
||||
fields={cronFields}
|
||||
onChange={handleFieldChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{showPresets && filteredPresets.length > 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
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Common cron expressions for quick setup
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<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
|
||||
key={index}
|
||||
variant="outline"
|
||||
className="h-auto justify-start p-4 text-left"
|
||||
onClick={() => handlePresetSelect(preset)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<div className="w-full space-y-2">
|
||||
<div className="font-medium text-sm">
|
||||
{preset.label}
|
||||
</div>
|
||||
<div className="whitespace-normal break-words text-muted-foreground text-xs leading-relaxed">
|
||||
{preset.description}
|
||||
</div>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="mt-1 break-all font-mono text-xs"
|
||||
>
|
||||
{preset.value}
|
||||
</Badge>
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
{/* Current Expression & Preview */}
|
||||
{showGeneratedExpression && (
|
||||
<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')}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="px-3 py-1 font-mono text-sm">
|
||||
{currentExpression}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{showPreview && nextRuns.length > 0 && (
|
||||
<>
|
||||
<Separator />
|
||||
<div className="space-y-2">
|
||||
<h4 className="flex items-center gap-2 font-medium text-sm">
|
||||
<Calendar className="h-4 w-4" />
|
||||
Next Runs({timezone})
|
||||
</h4>
|
||||
<div className="space-y-1">
|
||||
{nextRuns.map((date, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between rounded bg-muted/50 px-3 py-2 text-sm"
|
||||
>
|
||||
<span className="font-medium text-muted-foreground">
|
||||
#{index + 1}
|
||||
</span>
|
||||
|
||||
<span className="font-mono">
|
||||
{date.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface CronFieldEditorProps {
|
||||
period: CronPeriod;
|
||||
fields: Record<CronField, string>;
|
||||
onChange: (field: CronField, value: string) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const CronFieldEditor: FC<CronFieldEditorProps> = ({
|
||||
period,
|
||||
fields,
|
||||
onChange,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const relevantFields = [...PERIOD_CONFIGS[period].fields] as CronField[];
|
||||
|
||||
return (
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{relevantFields.map((field) => {
|
||||
const config = FIELD_CONFIGS[field];
|
||||
const currentValue = fields[field];
|
||||
|
||||
return (
|
||||
<div key={field} className="space-y-2">
|
||||
<Label className="font-medium text-sm capitalize">
|
||||
{field.replace(/([A-Z])/g, ' $1').toLowerCase()}
|
||||
</Label>
|
||||
|
||||
{field === 'month' || field === 'dayOfWeek' ? (
|
||||
<Select
|
||||
value={currentValue}
|
||||
onValueChange={(value) => onChange(field, value)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="*">Any</SelectItem>
|
||||
{config.options?.map((option, index) => (
|
||||
<SelectItem key={index} value={option.value.toString()}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
// biome-ignore lint/nursery/noNestedTernary: <explanation>
|
||||
) : field === 'dayOfMonth' ? (
|
||||
<div className="space-y-2">
|
||||
<Select
|
||||
value={currentValue}
|
||||
onValueChange={(value) => onChange(field, value)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{config.options?.map((option, index) => (
|
||||
<SelectItem key={index} value={option.value.toString()}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
{Array.from({ length: 31 }, (_, i) => i + 1).map((day) => (
|
||||
<SelectItem key={day} value={day.toString()}>
|
||||
{day}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={currentValue === '*' ? '*' : 'specific'}
|
||||
onValueChange={(value) => {
|
||||
if (value === '*') {
|
||||
onChange(field, '*');
|
||||
} else if (value === 'specific' && currentValue === '*') {
|
||||
onChange(field, '0');
|
||||
}
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
<ToggleGroupItem value="*" className="min-w-fit text-xs">
|
||||
Any
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value="specific"
|
||||
className="min-w-fit text-xs"
|
||||
>
|
||||
Specific
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
|
||||
{currentValue !== '*' && (
|
||||
<Input
|
||||
type="text"
|
||||
value={currentValue}
|
||||
onChange={(e) => onChange(field, e.target.value)}
|
||||
placeholder={`0-${config.max}`}
|
||||
disabled={disabled}
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="text-muted-foreground text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
<Info className="h-3 w-3" />
|
||||
<span>
|
||||
Range: {config.min}-{config.max}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
Supports: *, numbers, ranges (1-5), lists (1,3,5), steps
|
||||
(*/5)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function parseCronExpression(expression: string): Record<CronField, string> {
|
||||
const parts = expression.split(' ');
|
||||
|
||||
// Ensure we have 6 parts, pad with defaults if needed
|
||||
while (parts.length < 6) {
|
||||
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] || '*',
|
||||
};
|
||||
}
|
||||
|
||||
export { CronBuilder };
|
@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useMatches } from '@tanstack/react-router';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
@ -27,13 +27,8 @@ import {
|
||||
useSidebar,
|
||||
} from '@/components/ui/sidebar';
|
||||
import type { NavMainGroup, NavMainItem } from '@/infra/routes/nav';
|
||||
import { useMatches } from '@tanstack/react-router';
|
||||
|
||||
export function NavMain({
|
||||
groups,
|
||||
}: {
|
||||
groups: NavMainGroup[];
|
||||
}) {
|
||||
export function NavMain({ groups }: { groups: NavMainGroup[] }) {
|
||||
const matches = useMatches();
|
||||
const { state } = useSidebar();
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/presentation/utils";
|
||||
|
743
apps/webui/src/components/ui/cron/cron-builder.tsx
Normal file
743
apps/webui/src/components/ui/cron/cron-builder.tsx
Normal file
@ -0,0 +1,743 @@
|
||||
import { getFutureMatches } from "@datasert/cronjs-matcher";
|
||||
import { Calendar, Clock, Info, Settings, Zap } from "lucide-react";
|
||||
import {
|
||||
type CSSProperties,
|
||||
type FC,
|
||||
memo,
|
||||
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 { 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";
|
||||
import {
|
||||
type CronBuilderProps,
|
||||
CronField,
|
||||
type CronFieldConfig,
|
||||
CronPeriod,
|
||||
type CronPreset,
|
||||
} from "./types.js";
|
||||
|
||||
const CRON_PRESETS: CronPreset[] = [
|
||||
{
|
||||
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 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 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: "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: "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: "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",
|
||||
},
|
||||
];
|
||||
|
||||
const FIELD_CONFIGS: Record<CronField, CronFieldConfig> = {
|
||||
seconds: {
|
||||
min: 0,
|
||||
max: 59,
|
||||
step: 1,
|
||||
allowSpecial: ["*", "?"],
|
||||
},
|
||||
minutes: {
|
||||
min: 0,
|
||||
max: 59,
|
||||
step: 1,
|
||||
allowSpecial: ["*", "?"],
|
||||
},
|
||||
hours: {
|
||||
min: 0,
|
||||
max: 23,
|
||||
step: 1,
|
||||
allowSpecial: ["*", "?"],
|
||||
},
|
||||
dayOfMonth: {
|
||||
min: 1,
|
||||
max: 31,
|
||||
step: 1,
|
||||
allowSpecial: ["*", "?", "L", "W"],
|
||||
options: [
|
||||
{ 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: ["*"],
|
||||
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 },
|
||||
],
|
||||
},
|
||||
dayOfWeek: {
|
||||
min: 0,
|
||||
max: 6,
|
||||
step: 1,
|
||||
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 },
|
||||
],
|
||||
},
|
||||
year: {
|
||||
min: 0,
|
||||
max: 9999,
|
||||
step: 1,
|
||||
allowSpecial: ["*", "?"],
|
||||
},
|
||||
};
|
||||
|
||||
const PERIOD_CONFIGS = {
|
||||
minute: {
|
||||
label: CronPeriod.Minute,
|
||||
description: "Run every minute",
|
||||
template: "0 * * * * *",
|
||||
fields: [CronField.Minutes],
|
||||
},
|
||||
hourly: {
|
||||
label: CronPeriod.Hourly,
|
||||
description: "Run every hour",
|
||||
template: "0 0 * * * *",
|
||||
fields: [CronField.Minutes, CronField.Hours],
|
||||
},
|
||||
daily: {
|
||||
label: CronPeriod.Daily,
|
||||
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",
|
||||
fields: [
|
||||
CronField.Seconds,
|
||||
CronField.Minutes,
|
||||
CronField.Hours,
|
||||
CronField.DayOfWeek,
|
||||
],
|
||||
},
|
||||
monthly: {
|
||||
label: CronPeriod.Monthly,
|
||||
description: "Run every month",
|
||||
template: "0 0 0 1 * *",
|
||||
fields: [
|
||||
CronField.Seconds,
|
||||
CronField.Minutes,
|
||||
CronField.Hours,
|
||||
CronField.DayOfMonth,
|
||||
],
|
||||
},
|
||||
yearly: {
|
||||
label: CronPeriod.Yearly,
|
||||
description: "Run every year",
|
||||
template: "0 0 0 1 1 *",
|
||||
fields: [
|
||||
CronField.Seconds,
|
||||
CronField.Minutes,
|
||||
CronField.Hours,
|
||||
CronField.DayOfMonth,
|
||||
CronField.Month,
|
||||
],
|
||||
},
|
||||
custom: {
|
||||
label: CronPeriod.Custom,
|
||||
description: "Custom expression",
|
||||
template: "0 0 * * * *",
|
||||
fields: [
|
||||
CronField.Seconds,
|
||||
CronField.Minutes,
|
||||
CronField.Hours,
|
||||
CronField.DayOfMonth,
|
||||
CronField.Month,
|
||||
CronField.DayOfWeek,
|
||||
],
|
||||
},
|
||||
} as const;
|
||||
|
||||
const CronBuilder: FC<CronBuilderProps> = ({
|
||||
timezone = "UTC",
|
||||
value = "0 0 * * * *",
|
||||
onChange,
|
||||
className,
|
||||
disabled = false,
|
||||
showPreview = true,
|
||||
showPresets = true,
|
||||
displayPeriods = [
|
||||
CronPeriod.Custom,
|
||||
CronPeriod.Minute,
|
||||
CronPeriod.Hourly,
|
||||
CronPeriod.Daily,
|
||||
CronPeriod.Weekly,
|
||||
CronPeriod.Monthly,
|
||||
CronPeriod.Yearly,
|
||||
],
|
||||
defaultTab = CronPeriod.Custom,
|
||||
presets = CRON_PRESETS,
|
||||
showGeneratedExpression = true,
|
||||
withCard = true,
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState<CronPeriod>(defaultTab);
|
||||
const [cronFields, setCronFields] = useState(() =>
|
||||
parseCronExpression(value)
|
||||
);
|
||||
|
||||
const currentExpression = useMemo(() => {
|
||||
return `${cronFields.seconds} ${cronFields.minutes} ${cronFields.hours} ${cronFields.dayOfMonth} ${cronFields.month} ${cronFields.dayOfWeek}`;
|
||||
}, [cronFields]);
|
||||
|
||||
const nextRuns = useMemo(() => {
|
||||
if (!showPreview) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const matches = getFutureMatches(`${currentExpression} *`, {
|
||||
matchCount: 3,
|
||||
timezone,
|
||||
formatInTimezone: true,
|
||||
hasSeconds: true,
|
||||
});
|
||||
return matches.map((match) => new Date(match));
|
||||
} catch (error) {
|
||||
console.error("Failed to get future matched runs", error);
|
||||
return [];
|
||||
}
|
||||
}, [currentExpression, showPreview, timezone]);
|
||||
|
||||
useEffect(() => {
|
||||
setCronFields(parseCronExpression(value));
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
onChange?.(currentExpression);
|
||||
}, [currentExpression, onChange]);
|
||||
|
||||
const handlePresetSelect = useCallback((preset: CronPreset) => {
|
||||
setCronFields(parseCronExpression(preset.value));
|
||||
}, []);
|
||||
|
||||
const handleFieldChange = useCallback(
|
||||
(field: CronField, newValue: string) => {
|
||||
setCronFields((prev) => ({ ...prev, [field]: newValue }));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handlePeriodChange = useCallback((period: CronPeriod) => {
|
||||
setActiveTab(period);
|
||||
if (period !== "custom") {
|
||||
const config = PERIOD_CONFIGS[period];
|
||||
setCronFields(parseCronExpression(config.template));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const filteredPresets = useMemo(() => {
|
||||
return presets.filter((preset) => {
|
||||
if (activeTab === "custom") {
|
||||
return true;
|
||||
}
|
||||
return preset.category === activeTab;
|
||||
});
|
||||
}, [presets, activeTab]);
|
||||
|
||||
return (
|
||||
<div className={cn(withCard && "space-y-6", className)}>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={(v) => handlePeriodChange(v as CronPeriod)}
|
||||
>
|
||||
<div className="overflow-x-auto">
|
||||
<TabsList
|
||||
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":
|
||||
displayPeriods.length > 4
|
||||
? `${displayPeriods.length * 25 - 20}%`
|
||||
: "100%",
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
{displayPeriods.map((period) => (
|
||||
<TabsTrigger
|
||||
key={period}
|
||||
value={period}
|
||||
disabled={disabled}
|
||||
className="text-xs capitalize"
|
||||
>
|
||||
{PERIOD_CONFIGS[period].label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</div>
|
||||
{displayPeriods.map((period) => (
|
||||
<TabsContent
|
||||
key={period}
|
||||
value={period}
|
||||
className={cn(withCard ? "space-y-4" : "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">
|
||||
{PERIOD_CONFIGS[period].label} Configuration
|
||||
</span>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{PERIOD_CONFIGS[period].description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className={cn("space-y-4", !withCard && "px-0")}>
|
||||
<CronFieldEditor
|
||||
period={period}
|
||||
fields={cronFields}
|
||||
onChange={handleFieldChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{showPresets && filteredPresets.length > 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
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Common cron expressions for quick setup
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<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
|
||||
key={index}
|
||||
variant="outline"
|
||||
className="h-auto justify-start p-4 text-left"
|
||||
onClick={() => handlePresetSelect(preset)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<div className="w-full space-y-2">
|
||||
<div className="font-medium text-sm">
|
||||
{preset.label}
|
||||
</div>
|
||||
<div className="whitespace-normal break-words text-muted-foreground text-xs leading-relaxed">
|
||||
{preset.description}
|
||||
</div>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="mt-1 break-all font-mono text-xs"
|
||||
>
|
||||
{preset.value}
|
||||
</Badge>
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
{/* Current Expression & Preview */}
|
||||
{showGeneratedExpression && (
|
||||
<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")}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="px-3 py-1 font-mono text-sm">
|
||||
{currentExpression}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{showPreview && nextRuns.length > 0 && (
|
||||
<>
|
||||
<Separator />
|
||||
<div className="space-y-2">
|
||||
<h4 className="flex items-center gap-2 font-medium text-sm">
|
||||
<Calendar className="h-4 w-4" />
|
||||
Next Runs({timezone})
|
||||
</h4>
|
||||
<div className="space-y-1">
|
||||
{nextRuns.map((date, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between rounded bg-muted/50 px-3 py-2 text-sm"
|
||||
>
|
||||
<span className="font-medium text-muted-foreground">
|
||||
#{index + 1}
|
||||
</span>
|
||||
|
||||
<span className="font-mono">
|
||||
{date.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface CronFieldEditorProps {
|
||||
period: CronPeriod;
|
||||
fields: Record<CronField, string>;
|
||||
onChange: (field: CronField, value: string) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const CronFieldEditor: FC<CronFieldEditorProps> = ({
|
||||
period,
|
||||
fields,
|
||||
onChange,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const relevantFields = [...PERIOD_CONFIGS[period].fields] as CronField[];
|
||||
|
||||
return (
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{relevantFields.map((field) => {
|
||||
const config = FIELD_CONFIGS[field];
|
||||
const currentValue = fields[field];
|
||||
|
||||
return (
|
||||
<CronFieldItemEditor
|
||||
key={field}
|
||||
config={config}
|
||||
field={field}
|
||||
value={currentValue}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CronFieldItemAnyOrSpecificOption = {
|
||||
Any: "any",
|
||||
Specific: "specific",
|
||||
} as const;
|
||||
|
||||
type CronFieldItemAnyOrSpecificOption =
|
||||
(typeof CronFieldItemAnyOrSpecificOption)[keyof typeof CronFieldItemAnyOrSpecificOption];
|
||||
|
||||
interface CronFieldItemEditorProps {
|
||||
config: CronFieldConfig;
|
||||
field: CronField;
|
||||
value: string;
|
||||
onChange: (field: CronField, value: string) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
function encodeCronFieldItem(value: string): string {
|
||||
if (value === "") {
|
||||
return "<meta:empty>";
|
||||
}
|
||||
|
||||
if (value.includes(" ")) {
|
||||
return `<meta:contains-space:${encodeURIComponent(value)}>`;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function decodeCronFieldItem(value: string): string {
|
||||
if (value.startsWith("<meta:contains")) {
|
||||
return decodeURIComponent(
|
||||
// biome-ignore lint/performance/useTopLevelRegex: false
|
||||
value.replace(/^<meta:contains-space:([^>]+)>$/, "$1")
|
||||
);
|
||||
}
|
||||
|
||||
if (value === "<meta:empty>") {
|
||||
return "";
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export const CronFieldItemEditor: FC<CronFieldItemEditorProps> = memo(
|
||||
({ field, value, onChange, config, disabled = false }) => {
|
||||
const [innerValue, _setInnerValue] = useState(() =>
|
||||
decodeCronFieldItem(value)
|
||||
);
|
||||
|
||||
const [anyOrSpecificOption, _setAnyOrSpecificOption] =
|
||||
useState<CronFieldItemAnyOrSpecificOption>(() =>
|
||||
innerValue === "*"
|
||||
? CronFieldItemAnyOrSpecificOption.Any
|
||||
: CronFieldItemAnyOrSpecificOption.Specific
|
||||
);
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: false
|
||||
useEffect(() => {
|
||||
const nextValue = decodeCronFieldItem(value);
|
||||
if (nextValue !== innerValue) {
|
||||
_setInnerValue(nextValue);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(v: string) => {
|
||||
_setInnerValue(v);
|
||||
onChange(field, encodeCronFieldItem(v));
|
||||
},
|
||||
[field, onChange]
|
||||
);
|
||||
|
||||
const setAnyOrSpecificOption = useCallback(
|
||||
(v: CronFieldItemAnyOrSpecificOption) => {
|
||||
_setAnyOrSpecificOption(v);
|
||||
if (v === CronFieldItemAnyOrSpecificOption.Any) {
|
||||
handleChange("*");
|
||||
} else if (v === CronFieldItemAnyOrSpecificOption.Specific) {
|
||||
handleChange("0");
|
||||
}
|
||||
},
|
||||
[handleChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Label className="font-medium text-sm capitalize">
|
||||
{field.replace(/([A-Z])/g, " $1").toLowerCase()}
|
||||
</Label>
|
||||
|
||||
{(field === "month" || field === "dayOfWeek") && (
|
||||
<Select
|
||||
value={innerValue}
|
||||
onValueChange={handleChange}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="*">Any</SelectItem>
|
||||
{config.options?.map((option, index) => (
|
||||
<SelectItem key={index} value={option.value.toString()}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
{field === "dayOfMonth" && (
|
||||
<div className="space-y-2">
|
||||
<Select
|
||||
value={innerValue}
|
||||
onValueChange={handleChange}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{config.options?.map((option, index) => (
|
||||
<SelectItem key={index} value={option.value.toString()}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
{Array.from({ length: 31 }, (_, i) => i + 1).map((day) => (
|
||||
<SelectItem key={day} value={day.toString()}>
|
||||
{day}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
{!(
|
||||
field === "month" ||
|
||||
field === "dayOfWeek" ||
|
||||
field === "dayOfMonth"
|
||||
) && (
|
||||
<div className="space-y-2">
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={anyOrSpecificOption}
|
||||
onValueChange={setAnyOrSpecificOption}
|
||||
disabled={disabled}
|
||||
>
|
||||
<ToggleGroupItem
|
||||
value={CronFieldItemAnyOrSpecificOption.Any}
|
||||
className="min-w-fit text-xs"
|
||||
>
|
||||
Any
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value={CronFieldItemAnyOrSpecificOption.Specific}
|
||||
className="min-w-fit text-xs"
|
||||
>
|
||||
Specific
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
|
||||
{anyOrSpecificOption ===
|
||||
CronFieldItemAnyOrSpecificOption.Specific && (
|
||||
<Input
|
||||
type="text"
|
||||
value={innerValue}
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
placeholder={`0-${config.max}`}
|
||||
disabled={disabled}
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="text-muted-foreground text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
<Info className="h-3 w-3" />
|
||||
<span>
|
||||
Range: {config.min}-{config.max}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
Supports: *, numbers, ranges (1-5), lists (1,3,5), steps (*/5)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
function parseCronExpression(expression: string): Record<CronField, string> {
|
||||
const parts = expression.split(" ");
|
||||
|
||||
// Ensure we have 6 parts, pad with defaults if needed
|
||||
while (parts.length < 6) {
|
||||
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] || "*",
|
||||
};
|
||||
}
|
||||
|
||||
export { CronBuilder };
|
@ -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>
|
@ -33,7 +33,11 @@ const CronInput = forwardRef<HTMLInputElement, CronInputProps>(
|
||||
|
||||
const validationResult = useMemo((): CronValidationResult => {
|
||||
if (!internalValue.trim()) {
|
||||
return { isValid: false, error: 'Expression is required' };
|
||||
return {
|
||||
isValid: false,
|
||||
error: 'Expression is required',
|
||||
isEmpty: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
@ -1,16 +1,4 @@
|
||||
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 { parse } from '@datasert/cronjs-parser';
|
||||
import { parse } from "@datasert/cronjs-parser";
|
||||
import {
|
||||
AlertCircle,
|
||||
Bolt,
|
||||
@ -19,33 +7,45 @@ import {
|
||||
Copy,
|
||||
Settings,
|
||||
Type,
|
||||
} from 'lucide-react';
|
||||
import { type FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { CronBuilder } from './cron-builder';
|
||||
import { CronDisplay } from './cron-display';
|
||||
import { CronInput } from './cron-input';
|
||||
} 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.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,
|
||||
@ -55,9 +55,9 @@ const Cron: FC<CronProps> = ({
|
||||
showPresets,
|
||||
withCard = true,
|
||||
isFirstSibling = false,
|
||||
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: <explanation>
|
||||
// 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]);
|
||||
@ -106,9 +106,9 @@ const Cron: FC<CronProps> = ({
|
||||
);
|
||||
|
||||
const handleActiveModeChange = useCallback(
|
||||
(mode: CronPrimitiveMode) => {
|
||||
setInternalActiveMode(mode);
|
||||
onActiveModeChange?.(mode);
|
||||
(m: CronPrimitiveMode) => {
|
||||
setInternalActiveMode(m);
|
||||
onActiveModeChange?.(m);
|
||||
},
|
||||
[onActiveModeChange]
|
||||
);
|
||||
@ -122,17 +122,17 @@ const Cron: FC<CronProps> = ({
|
||||
await navigator.clipboard.writeText(internalValue);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (error) {
|
||||
console.warn('Failed to copy to clipboard:', error);
|
||||
} catch (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={(value) =>
|
||||
handleActiveModeChange(value as 'input' | 'builder')
|
||||
onValueChange={(v) =>
|
||||
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">
|
@ -1,20 +1,20 @@
|
||||
export { Cron } from './cron';
|
||||
export { CronInput } from './cron-input';
|
||||
export { CronBuilder } from './cron-builder';
|
||||
export { CronDisplay } from './cron-display';
|
||||
export { CronExample } from './cron-example';
|
||||
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 CronProps,
|
||||
type CronInputProps,
|
||||
type CronBuilderProps,
|
||||
type CronDisplayProps,
|
||||
type CronExpression,
|
||||
CronField,
|
||||
type CronFieldConfig,
|
||||
type CronInputProps,
|
||||
type CronNextRun,
|
||||
CronPeriod,
|
||||
type CronPreset,
|
||||
type CronProps,
|
||||
type CronValidationResult,
|
||||
type CronNextRun,
|
||||
type CronFieldConfig,
|
||||
CronField,
|
||||
type PeriodConfig,
|
||||
} from './types';
|
||||
} from "./types.js";
|
@ -1,20 +1,32 @@
|
||||
import { type LinkComponent, createLink } from "@tanstack/react-router";
|
||||
import type { AnchorHTMLAttributes, ComponentProps } from "react";
|
||||
import { createLink, type LinkComponentProps } from "@tanstack/react-router";
|
||||
import type { AnchorHTMLAttributes } from "react";
|
||||
|
||||
export interface BasicLinkProps
|
||||
extends AnchorHTMLAttributes<HTMLAnchorElement> {}
|
||||
extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
|
||||
href: string;
|
||||
to?: undefined;
|
||||
}
|
||||
|
||||
const BasicLinkComponent = (props: ComponentProps<"a">) => {
|
||||
const BasicLinkComponent = (props: BasicLinkProps) => {
|
||||
return <a {...props} />;
|
||||
};
|
||||
|
||||
const CreatedLinkComponent = createLink(BasicLinkComponent);
|
||||
|
||||
export const ProLink: LinkComponent<typeof BasicLinkComponent> = (props) => {
|
||||
export const ProLink = (
|
||||
props: LinkComponentProps<typeof BasicLinkComponent> | BasicLinkProps
|
||||
) => {
|
||||
if (props.href) {
|
||||
return <BasicLinkComponent {...(props as any)} />;
|
||||
}
|
||||
return <CreatedLinkComponent preload={"intent"} {...props} />;
|
||||
return (
|
||||
<CreatedLinkComponent
|
||||
preload={"intent"}
|
||||
{...(props as LinkComponentProps<typeof BasicLinkComponent>)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export type ProLinkProps = ComponentProps<typeof ProLink>;
|
||||
export type ProLinkProps =
|
||||
| LinkComponentProps<typeof BasicLinkComponent>
|
||||
| BasicLinkProps;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
|
||||
export interface QueryErrorViewProps {
|
||||
title?: string;
|
||||
|
@ -1,6 +1,5 @@
|
||||
import type { CronPreset } from '@/components/domains/cron';
|
||||
import type { GetCronsQuery } from '@/infra/graphql/gql/graphql';
|
||||
import { gql } from '@apollo/client';
|
||||
import type { GetCronsQuery } from '@/infra/graphql/gql/graphql';
|
||||
|
||||
export const GET_CRONS = gql`
|
||||
query GetCrons($filter: CronFilterInput!, $orderBy: CronOrderInput!, $pagination: PaginationInput!) {
|
||||
|
@ -1,16 +1,16 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import { type } from 'arktype';
|
||||
import { arkValidatorToTypeNarrower } from '@/infra/errors/arktype';
|
||||
import {
|
||||
type GetSubscriptionsQuery,
|
||||
SubscriptionCategoryEnum,
|
||||
} from '@/infra/graphql/gql/graphql';
|
||||
import { gql } from '@apollo/client';
|
||||
import { type } from 'arktype';
|
||||
import {
|
||||
extractMikanSubscriptionBangumiSourceUrl,
|
||||
extractMikanSubscriptionSubscriberSourceUrl,
|
||||
MikanSubscriptionBangumiSourceUrlSchema,
|
||||
MikanSubscriptionSeasonSourceUrlSchema,
|
||||
MikanSubscriptionSubscriberSourceUrlSchema,
|
||||
extractMikanSubscriptionBangumiSourceUrl,
|
||||
extractMikanSubscriptionSubscriberSourceUrl,
|
||||
} from './mikan';
|
||||
|
||||
export const GET_SUBSCRIPTIONS = gql`
|
||||
@ -83,10 +83,8 @@ export const DELETE_SUBSCRIPTIONS = gql`
|
||||
`;
|
||||
|
||||
export const GET_SUBSCRIPTION_DETAIL = gql`
|
||||
query GetSubscriptionDetail ($id: Int!) {
|
||||
subscriptions(filter: { id: {
|
||||
eq: $id
|
||||
} }) {
|
||||
query GetSubscriptionDetail ($filter: SubscriptionsFilterInput!) {
|
||||
subscriptions(filter: $filter) {
|
||||
nodes {
|
||||
id
|
||||
subscriberId
|
||||
@ -106,7 +104,15 @@ query GetSubscriptionDetail ($id: Int!) {
|
||||
feedSource
|
||||
}
|
||||
}
|
||||
subscriberTask {
|
||||
subscriberTask(pagination: {
|
||||
page: {
|
||||
page: 0,
|
||||
limit: 3,
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
runAt: DESC,
|
||||
}) {
|
||||
nodes {
|
||||
id
|
||||
taskType
|
||||
@ -117,7 +123,15 @@ query GetSubscriptionDetail ($id: Int!) {
|
||||
id
|
||||
username
|
||||
}
|
||||
cron {
|
||||
cron (pagination: {
|
||||
page: {
|
||||
page: 0,
|
||||
limit: 3,
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: DESC,
|
||||
}) {
|
||||
nodes {
|
||||
id
|
||||
cronExpr
|
||||
|
@ -1,5 +1,5 @@
|
||||
import type { GetTasksQuery } from '@/infra/graphql/gql/graphql';
|
||||
import { gql } from '@apollo/client';
|
||||
import type { GetTasksQuery } from '@/infra/graphql/gql/graphql';
|
||||
|
||||
export const GET_TASKS = gql`
|
||||
query GetTasks($filter: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {
|
||||
|
30
apps/webui/src/infra/forms/compat.ts
Normal file
30
apps/webui/src/infra/forms/compat.ts
Normal file
@ -0,0 +1,30 @@
|
||||
type AllKeys<T> = T extends any ? keyof T : never;
|
||||
|
||||
type ToDefaultable<T> = Exclude<
|
||||
T extends string | undefined
|
||||
? T | ''
|
||||
: T extends number | undefined
|
||||
? T | number
|
||||
: T extends undefined
|
||||
? T | null
|
||||
: T,
|
||||
undefined
|
||||
>;
|
||||
|
||||
type PickFieldFormUnion<T, K extends keyof T> = T extends any
|
||||
? T[keyof T & K]
|
||||
: never;
|
||||
|
||||
// compact more types;
|
||||
export type FormDefaultValues<T> = {
|
||||
-readonly [K in AllKeys<T>]-?: ToDefaultable<PickFieldFormUnion<T, K>>;
|
||||
};
|
||||
|
||||
/**
|
||||
* https://github.com/shadcn-ui/ui/issues/427
|
||||
*/
|
||||
export function compatFormDefaultValues<T, K extends AllKeys<T> = AllKeys<T>>(
|
||||
d: FormDefaultValues<Pick<T, K>>
|
||||
): T {
|
||||
return d as unknown as T;
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
/* eslint-disable */
|
||||
import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core';
|
||||
import { FragmentDefinitionNode } from 'graphql';
|
||||
import { Incremental } from './graphql';
|
||||
import type { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core';
|
||||
import type { FragmentDefinitionNode } from 'graphql';
|
||||
import type { Incremental } from './graphql';
|
||||
|
||||
|
||||
export type FragmentType<TDocumentType extends DocumentTypeDecoration<any, any>> = TDocumentType extends DocumentTypeDecoration<
|
||||
|
@ -1,6 +1,6 @@
|
||||
/* eslint-disable */
|
||||
import * as types from './graphql';
|
||||
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
|
||||
import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
|
||||
|
||||
/**
|
||||
* Map of all GraphQL operations in the project.
|
||||
@ -30,8 +30,8 @@ type Documents = {
|
||||
"\n mutation InsertSubscription($data: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n }\n": typeof types.InsertSubscriptionDocument,
|
||||
"\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filter: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filter\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": typeof types.UpdateSubscriptionsDocument,
|
||||
"\n mutation DeleteSubscriptions($filter: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filter)\n }\n": typeof types.DeleteSubscriptionsDocument,
|
||||
"\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n cron {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n enabled\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n }\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n": typeof types.GetSubscriptionDetailDocument,
|
||||
"\n query GetTasks($filter: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filter: $filter\n orderBy: $orderBy\n ) {\n nodes {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority,\n subscription {\n displayName\n sourceUrl\n cron {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n }\n }\n }\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": typeof types.GetTasksDocument,
|
||||
"\nquery GetSubscriptionDetail ($filter: SubscriptionsFilterInput!) {\n subscriptions(filter: $filter) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask(pagination: {\n page: {\n page: 0,\n limit: 3,\n }\n },\n orderBy: {\n runAt: DESC,\n }) {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n cron (pagination: {\n page: {\n page: 0,\n limit: 3,\n }\n },\n orderBy: {\n createdAt: DESC,\n }) {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n enabled\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n }\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n": typeof types.GetSubscriptionDetailDocument,
|
||||
"\n query GetTasks($filter: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filter: $filter\n orderBy: $orderBy\n ) {\n nodes {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority,\n subscription {\n displayName\n sourceUrl\n }\n cron {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n }\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": typeof types.GetTasksDocument,
|
||||
"\n mutation InsertSubscriberTask($data: SubscriberTasksInsertInput!) {\n subscriberTasksCreateOne(data: $data) {\n id\n }\n }\n": typeof types.InsertSubscriberTaskDocument,
|
||||
"\n mutation DeleteTasks($filter: SubscriberTasksFilterInput!) {\n subscriberTasksDelete(filter: $filter)\n }\n": typeof types.DeleteTasksDocument,
|
||||
"\n mutation RetryTasks($filter: SubscriberTasksFilterInput!) {\n subscriberTasksRetryOne(filter: $filter) {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority\n }\n }\n": typeof types.RetryTasksDocument,
|
||||
@ -53,8 +53,8 @@ const documents: Documents = {
|
||||
"\n mutation InsertSubscription($data: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n }\n": types.InsertSubscriptionDocument,
|
||||
"\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filter: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filter\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": types.UpdateSubscriptionsDocument,
|
||||
"\n mutation DeleteSubscriptions($filter: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filter)\n }\n": types.DeleteSubscriptionsDocument,
|
||||
"\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n cron {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n enabled\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n }\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n": types.GetSubscriptionDetailDocument,
|
||||
"\n query GetTasks($filter: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filter: $filter\n orderBy: $orderBy\n ) {\n nodes {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority,\n subscription {\n displayName\n sourceUrl\n cron {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n }\n }\n }\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": types.GetTasksDocument,
|
||||
"\nquery GetSubscriptionDetail ($filter: SubscriptionsFilterInput!) {\n subscriptions(filter: $filter) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask(pagination: {\n page: {\n page: 0,\n limit: 3,\n }\n },\n orderBy: {\n runAt: DESC,\n }) {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n cron (pagination: {\n page: {\n page: 0,\n limit: 3,\n }\n },\n orderBy: {\n createdAt: DESC,\n }) {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n enabled\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n }\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n": types.GetSubscriptionDetailDocument,
|
||||
"\n query GetTasks($filter: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filter: $filter\n orderBy: $orderBy\n ) {\n nodes {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority,\n subscription {\n displayName\n sourceUrl\n }\n cron {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n }\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": types.GetTasksDocument,
|
||||
"\n mutation InsertSubscriberTask($data: SubscriberTasksInsertInput!) {\n subscriberTasksCreateOne(data: $data) {\n id\n }\n }\n": types.InsertSubscriberTaskDocument,
|
||||
"\n mutation DeleteTasks($filter: SubscriberTasksFilterInput!) {\n subscriberTasksDelete(filter: $filter)\n }\n": types.DeleteTasksDocument,
|
||||
"\n mutation RetryTasks($filter: SubscriberTasksFilterInput!) {\n subscriberTasksRetryOne(filter: $filter) {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority\n }\n }\n": types.RetryTasksDocument,
|
||||
@ -141,11 +141,11 @@ export function gql(source: "\n mutation DeleteSubscriptions($filter: Subscri
|
||||
/**
|
||||
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function gql(source: "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n cron {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n enabled\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n }\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n"): (typeof documents)["\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n cron {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n enabled\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n }\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n"];
|
||||
export function gql(source: "\nquery GetSubscriptionDetail ($filter: SubscriptionsFilterInput!) {\n subscriptions(filter: $filter) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask(pagination: {\n page: {\n page: 0,\n limit: 3,\n }\n },\n orderBy: {\n runAt: DESC,\n }) {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n cron (pagination: {\n page: {\n page: 0,\n limit: 3,\n }\n },\n orderBy: {\n createdAt: DESC,\n }) {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n enabled\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n }\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n"): (typeof documents)["\nquery GetSubscriptionDetail ($filter: SubscriptionsFilterInput!) {\n subscriptions(filter: $filter) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask(pagination: {\n page: {\n page: 0,\n limit: 3,\n }\n },\n orderBy: {\n runAt: DESC,\n }) {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n cron (pagination: {\n page: {\n page: 0,\n limit: 3,\n }\n },\n orderBy: {\n createdAt: DESC,\n }) {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n enabled\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n }\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n"];
|
||||
/**
|
||||
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function gql(source: "\n query GetTasks($filter: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filter: $filter\n orderBy: $orderBy\n ) {\n nodes {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority,\n subscription {\n displayName\n sourceUrl\n cron {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n }\n }\n }\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n"): (typeof documents)["\n query GetTasks($filter: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filter: $filter\n orderBy: $orderBy\n ) {\n nodes {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority,\n subscription {\n displayName\n sourceUrl\n cron {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n }\n }\n }\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n"];
|
||||
export function gql(source: "\n query GetTasks($filter: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filter: $filter\n orderBy: $orderBy\n ) {\n nodes {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority,\n subscription {\n displayName\n sourceUrl\n }\n cron {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n }\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n"): (typeof documents)["\n query GetTasks($filter: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filter: $filter\n orderBy: $orderBy\n ) {\n nodes {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority,\n subscription {\n displayName\n sourceUrl\n }\n cron {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n }\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n"];
|
||||
/**
|
||||
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
File diff suppressed because one or more lines are too long
@ -1,8 +1,8 @@
|
||||
import { AUTH_PROVIDER } from '@/infra/auth/auth.provider';
|
||||
import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client';
|
||||
import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client';
|
||||
import { setContext } from '@apollo/client/link/context';
|
||||
import { Injectable, inject } from '@outposts/injection-js';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { AUTH_PROVIDER } from '@/infra/auth/auth.provider';
|
||||
|
||||
@Injectable()
|
||||
export class GraphQLService {
|
||||
|
@ -4,11 +4,18 @@ import { DOCUMENT } from '../platform/injection';
|
||||
export class IntlService {
|
||||
document = inject(DOCUMENT);
|
||||
|
||||
get timezone() {
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
get Intl(): typeof Intl {
|
||||
return this.document.defaultView?.Intl as typeof Intl;
|
||||
}
|
||||
|
||||
formatTimestamp(timestamp: number, options?: Intl.DateTimeFormatOptions) {
|
||||
get timezone() {
|
||||
return this.Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
}
|
||||
|
||||
formatDatetimeWithTz(
|
||||
timestamp: number | string | Date,
|
||||
options?: Intl.DateTimeFormatOptions
|
||||
) {
|
||||
const defaultOptions: Intl.DateTimeFormatOptions = {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
@ -20,7 +27,7 @@ export class IntlService {
|
||||
...options,
|
||||
};
|
||||
|
||||
return new Intl.DateTimeFormat(
|
||||
return new this.Intl.DateTimeFormat(
|
||||
this.document.defaultView?.navigator.language,
|
||||
{
|
||||
...defaultOptions,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type { ProLinkProps } from '@/components/ui/pro-link';
|
||||
import { type } from 'arktype';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import type { ProLinkProps } from '@/components/ui/pro-link';
|
||||
|
||||
export interface NavMainItem {
|
||||
link?: ProLinkProps;
|
||||
|
@ -1,3 +1,13 @@
|
||||
import { useMutation } from '@apollo/client';
|
||||
import {
|
||||
createFileRoute,
|
||||
useCanGoBack,
|
||||
useNavigate,
|
||||
useRouter,
|
||||
} from '@tanstack/react-router';
|
||||
import { type } from 'arktype';
|
||||
import { Loader2, Save } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
@ -24,7 +34,9 @@ import {
|
||||
INSERT_CREDENTIAL_3RD,
|
||||
} from '@/domains/recorder/schema/credential3rd';
|
||||
import { useInject } from '@/infra/di/inject';
|
||||
import { compatFormDefaultValues } from '@/infra/forms/compat';
|
||||
import {
|
||||
type Credential3rdInsertInput,
|
||||
Credential3rdTypeEnum,
|
||||
type InsertCredential3rdMutation,
|
||||
type InsertCredential3rdMutationVariables,
|
||||
@ -35,16 +47,6 @@ import {
|
||||
CreateCompleteActionSchema,
|
||||
} from '@/infra/routes/nav';
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import {
|
||||
createFileRoute,
|
||||
useCanGoBack,
|
||||
useNavigate,
|
||||
useRouter,
|
||||
} from '@tanstack/react-router';
|
||||
import { type } from 'arktype';
|
||||
import { Loader2, Save } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
const RouteSearchSchema = type({
|
||||
completeAction: CreateCompleteActionSchema.optional(),
|
||||
@ -98,21 +100,24 @@ function CredentialCreateRouteComponent() {
|
||||
});
|
||||
|
||||
const form = useAppForm({
|
||||
defaultValues: {
|
||||
defaultValues: compatFormDefaultValues<
|
||||
Credential3rdInsertInput,
|
||||
'credentialType' | 'username' | 'password' | 'userAgent'
|
||||
>({
|
||||
credentialType: Credential3rdTypeEnum.Mikan,
|
||||
username: '',
|
||||
password: '',
|
||||
userAgent: '',
|
||||
},
|
||||
}),
|
||||
validators: {
|
||||
onChangeAsync: Credential3rdInsertSchema,
|
||||
onChangeAsyncDebounceMs: 300,
|
||||
onSubmit: Credential3rdInsertSchema,
|
||||
},
|
||||
onSubmit: async (form) => {
|
||||
onSubmit: async (submittedForm) => {
|
||||
const value = {
|
||||
...form.value,
|
||||
userAgent: form.value.userAgent || platformService.userAgent,
|
||||
...submittedForm.value,
|
||||
userAgent: submittedForm.value.userAgent || platformService.userAgent,
|
||||
};
|
||||
await insertCredential3rd({
|
||||
variables: {
|
||||
@ -183,7 +188,7 @@ function CredentialCreateRouteComponent() {
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
value={field.state.value ?? ''}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
placeholder="Please enter username"
|
||||
@ -207,7 +212,7 @@ function CredentialCreateRouteComponent() {
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
type="password"
|
||||
value={field.state.value}
|
||||
value={field.state.value ?? ''}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
placeholder="Please enter password"
|
||||
|
@ -1,3 +1,7 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import { CheckIcon, Edit, Eye, EyeOff } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
@ -15,13 +19,10 @@ import { Label } from '@/components/ui/label';
|
||||
import { QueryErrorView } from '@/components/ui/query-error-view';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { GET_CREDENTIAL_3RD_DETAIL } from '@/domains/recorder/schema/credential3rd';
|
||||
import { useInject } from '@/infra/di/inject';
|
||||
import type { GetCredential3rdDetailQuery } from '@/infra/graphql/gql/graphql';
|
||||
import { IntlService } from '@/infra/intl/intl.service';
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import { format } from 'date-fns/format';
|
||||
import { CheckIcon, Edit, Eye, EyeOff } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { Credential3rdCheckAvailableViewDialogContent } from './-check-available';
|
||||
|
||||
export const Route = createFileRoute('/_app/credential3rd/detail/$id')({
|
||||
@ -34,6 +35,7 @@ export const Route = createFileRoute('/_app/credential3rd/detail/$id')({
|
||||
function Credential3rdDetailRouteComponent() {
|
||||
const { id } = Route.useParams();
|
||||
const navigate = useNavigate();
|
||||
const intlService = useInject(IntlService);
|
||||
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
@ -41,7 +43,7 @@ function Credential3rdDetailRouteComponent() {
|
||||
GET_CREDENTIAL_3RD_DETAIL,
|
||||
{
|
||||
variables: {
|
||||
id: Number.parseInt(id),
|
||||
id: Number.parseInt(id, 10),
|
||||
},
|
||||
}
|
||||
);
|
||||
@ -177,10 +179,7 @@ function Credential3rdDetailRouteComponent() {
|
||||
<Label className="font-medium text-sm">Created at</Label>
|
||||
<div className="rounded-md bg-muted p-3">
|
||||
<span className="text-sm">
|
||||
{format(
|
||||
new Date(credential.createdAt),
|
||||
'yyyy-MM-dd HH:mm:ss'
|
||||
)}
|
||||
{intlService.formatDatetimeWithTz(credential.createdAt)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -189,10 +188,7 @@ function Credential3rdDetailRouteComponent() {
|
||||
<Label className="font-medium text-sm">Updated at</Label>
|
||||
<div className="rounded-md bg-muted p-3">
|
||||
<span className="text-sm">
|
||||
{format(
|
||||
new Date(credential.updatedAt),
|
||||
'yyyy-MM-dd HH:mm:ss'
|
||||
)}
|
||||
{intlService.formatDatetimeWithTz(credential.updatedAt)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,3 +1,8 @@
|
||||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { Eye, EyeOff, Save } from 'lucide-react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
@ -32,18 +37,15 @@ import {
|
||||
apolloErrorToMessage,
|
||||
getApolloQueryError,
|
||||
} from '@/infra/errors/apollo';
|
||||
import { compatFormDefaultValues } from '@/infra/forms/compat';
|
||||
import type {
|
||||
Credential3rdTypeEnum,
|
||||
Credential3rdUpdateInput,
|
||||
GetCredential3rdDetailQuery,
|
||||
UpdateCredential3rdMutation,
|
||||
UpdateCredential3rdMutationVariables,
|
||||
} from '@/infra/graphql/gql/graphql';
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { Eye, EyeOff, Save } from 'lucide-react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export const Route = createFileRoute('/_app/credential3rd/edit/$id')({
|
||||
component: Credential3rdEditRouteComponent,
|
||||
@ -77,18 +79,21 @@ function FormView({
|
||||
});
|
||||
|
||||
const form = useAppForm({
|
||||
defaultValues: {
|
||||
defaultValues: compatFormDefaultValues<
|
||||
Credential3rdUpdateInput,
|
||||
'credentialType' | 'username' | 'password' | 'userAgent'
|
||||
>({
|
||||
credentialType: credential.credentialType,
|
||||
username: credential.username,
|
||||
password: credential.password,
|
||||
userAgent: credential.userAgent,
|
||||
},
|
||||
username: credential.username ?? '',
|
||||
password: credential.password ?? '',
|
||||
userAgent: credential.userAgent ?? '',
|
||||
}),
|
||||
validators: {
|
||||
onBlur: Credential3rdUpdateSchema,
|
||||
onSubmit: Credential3rdUpdateSchema,
|
||||
},
|
||||
onSubmit: (form) => {
|
||||
const value = form.value;
|
||||
onSubmit: (submittedForm) => {
|
||||
const value = submittedForm.value;
|
||||
updateCredential({
|
||||
variables: {
|
||||
data: value,
|
||||
@ -238,7 +243,7 @@ function Credential3rdEditRouteComponent() {
|
||||
const { loading, error, data, refetch } =
|
||||
useQuery<GetCredential3rdDetailQuery>(GET_CREDENTIAL_3RD_DETAIL, {
|
||||
variables: {
|
||||
id: Number.parseInt(id),
|
||||
id: Number.parseInt(id, 10),
|
||||
},
|
||||
});
|
||||
|
||||
@ -246,10 +251,10 @@ function Credential3rdEditRouteComponent() {
|
||||
|
||||
const onCompleted = useCallback(async () => {
|
||||
const refetchResult = await refetch();
|
||||
const error = getApolloQueryError(refetchResult);
|
||||
if (error) {
|
||||
const _error = getApolloQueryError(refetchResult);
|
||||
if (_error) {
|
||||
toast.error('Update credential failed', {
|
||||
description: apolloErrorToMessage(error),
|
||||
description: apolloErrorToMessage(_error),
|
||||
});
|
||||
} else {
|
||||
toast.success('Update credential successfully');
|
||||
|
@ -1,3 +1,20 @@
|
||||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import { Dialog } from '@radix-ui/react-dialog';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import {
|
||||
type ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
type PaginationState,
|
||||
type Row,
|
||||
type SortingState,
|
||||
useReactTable,
|
||||
type VisibilityState,
|
||||
} from '@tanstack/react-table';
|
||||
import { Eye, EyeOff, Plus } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
@ -21,33 +38,17 @@ import {
|
||||
DELETE_CREDENTIAL_3RD,
|
||||
GET_CREDENTIAL_3RD,
|
||||
} from '@/domains/recorder/schema/credential3rd';
|
||||
import { useInject } from '@/infra/di/inject';
|
||||
import {
|
||||
apolloErrorToMessage,
|
||||
getApolloQueryError,
|
||||
} from '@/infra/errors/apollo';
|
||||
import type { GetCredential3rdQuery } from '@/infra/graphql/gql/graphql';
|
||||
import { IntlService } from '@/infra/intl/intl.service';
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { useDebouncedSkeleton } from '@/presentation/hooks/use-debounded-skeleton';
|
||||
import { useEvent } from '@/presentation/hooks/use-event';
|
||||
import { cn } from '@/presentation/utils';
|
||||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import { Dialog } from '@radix-ui/react-dialog';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import {
|
||||
type ColumnDef,
|
||||
type PaginationState,
|
||||
type Row,
|
||||
type SortingState,
|
||||
type VisibilityState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
import { format } from 'date-fns';
|
||||
import { Eye, EyeOff, Plus } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { Credential3rdCheckAvailableViewDialogContent } from './-check-available';
|
||||
|
||||
export const Route = createFileRoute('/_app/credential3rd/manage')({
|
||||
@ -59,6 +60,7 @@ export const Route = createFileRoute('/_app/credential3rd/manage')({
|
||||
|
||||
function CredentialManageRouteComponent() {
|
||||
const navigate = useNavigate();
|
||||
const intlService = useInject(IntlService);
|
||||
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({
|
||||
createdAt: false,
|
||||
@ -94,18 +96,18 @@ function CredentialManageRouteComponent() {
|
||||
const [deleteCredential] = useMutation(DELETE_CREDENTIAL_3RD, {
|
||||
onCompleted: async () => {
|
||||
const refetchResult = await refetch();
|
||||
const error = getApolloQueryError(refetchResult);
|
||||
if (error) {
|
||||
const e = getApolloQueryError(refetchResult);
|
||||
if (e) {
|
||||
toast.error('Failed to delete credential', {
|
||||
description: apolloErrorToMessage(error),
|
||||
description: apolloErrorToMessage(e),
|
||||
});
|
||||
return;
|
||||
}
|
||||
toast.success('Credential deleted');
|
||||
},
|
||||
onError: (error) => {
|
||||
onError: (e) => {
|
||||
toast.error('Failed to delete credential', {
|
||||
description: error.message,
|
||||
description: e.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
@ -212,7 +214,7 @@ function CredentialManageRouteComponent() {
|
||||
const createdAt = row.original.createdAt;
|
||||
return (
|
||||
<div className="text-sm">
|
||||
{format(new Date(createdAt), 'yyyy-MM-dd HH:mm:ss')}
|
||||
{intlService.formatDatetimeWithTz(createdAt)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@ -224,7 +226,7 @@ function CredentialManageRouteComponent() {
|
||||
const updatedAt = row.original.updatedAt;
|
||||
return (
|
||||
<div className="text-sm">
|
||||
{format(new Date(updatedAt), 'yyyy-MM-dd HH:mm:ss')}
|
||||
{intlService.formatDatetimeWithTz(updatedAt)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@ -266,7 +268,13 @@ function CredentialManageRouteComponent() {
|
||||
},
|
||||
];
|
||||
return cs;
|
||||
}, [handleDeleteRecord, navigate, showPasswords, togglePasswordVisibility]);
|
||||
}, [
|
||||
handleDeleteRecord,
|
||||
navigate,
|
||||
showPasswords,
|
||||
togglePasswordVisibility,
|
||||
intlService.formatDatetimeWithTz,
|
||||
]);
|
||||
|
||||
const table = useReactTable({
|
||||
data: useMemo(() => credentials?.nodes ?? [], [credentials]),
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -1,3 +1,7 @@
|
||||
import { useMutation } from '@apollo/client';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import { Loader2, Save } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
@ -27,6 +31,7 @@ import {
|
||||
} from '@/domains/recorder/schema/subscriptions';
|
||||
import { SubscriptionService } from '@/domains/recorder/services/subscription.service';
|
||||
import { useInject } from '@/infra/di/inject';
|
||||
import { compatFormDefaultValues } from '@/infra/forms/compat';
|
||||
import {
|
||||
Credential3rdTypeEnum,
|
||||
type InsertSubscriptionMutation,
|
||||
@ -34,11 +39,6 @@ import {
|
||||
SubscriptionCategoryEnum,
|
||||
} from '@/infra/graphql/gql/graphql';
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { Loader2, Save } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { Credential3rdSelectContent } from './-credential3rd-select';
|
||||
|
||||
export const Route = createFileRoute('/_app/subscriptions/create')({
|
||||
@ -71,22 +71,24 @@ function SubscriptionCreateRouteComponent() {
|
||||
});
|
||||
|
||||
const form = useAppForm({
|
||||
defaultValues: {
|
||||
defaultValues: compatFormDefaultValues<SubscriptionForm>({
|
||||
displayName: '',
|
||||
category: undefined,
|
||||
category: '',
|
||||
enabled: true,
|
||||
sourceUrl: '',
|
||||
credentialId: '',
|
||||
year: undefined,
|
||||
credentialId: Number.NaN,
|
||||
year: Number.NaN,
|
||||
seasonStr: '',
|
||||
} as unknown as SubscriptionForm,
|
||||
}),
|
||||
validators: {
|
||||
onChangeAsync: SubscriptionFormSchema,
|
||||
onChangeAsyncDebounceMs: 300,
|
||||
onSubmit: SubscriptionFormSchema,
|
||||
},
|
||||
onSubmit: async (form) => {
|
||||
const input = subscriptionService.transformInsertFormToInput(form.value);
|
||||
onSubmit: async (submittedForm) => {
|
||||
const input = subscriptionService.transformInsertFormToInput(
|
||||
submittedForm.value
|
||||
);
|
||||
await insertSubscription({
|
||||
variables: {
|
||||
data: input,
|
||||
@ -119,30 +121,6 @@ function SubscriptionCreateRouteComponent() {
|
||||
}}
|
||||
className="space-y-6"
|
||||
>
|
||||
<form.Field name="displayName">
|
||||
{(field) => (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={field.name}>Display Name *</Label>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
placeholder="Please enter display name"
|
||||
autoComplete="off"
|
||||
/>
|
||||
{field.state.meta.errors && (
|
||||
<FormFieldErrors
|
||||
errors={field.state.meta.errors}
|
||||
isDirty={field.state.meta.isDirty}
|
||||
submissionAttempts={form.state.submissionAttempts}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</form.Field>
|
||||
|
||||
<form.Field name="category">
|
||||
{(field) => (
|
||||
<div className="space-y-2">
|
||||
@ -192,7 +170,7 @@ function SubscriptionCreateRouteComponent() {
|
||||
<Select
|
||||
value={field.state.value.toString()}
|
||||
onValueChange={(value) =>
|
||||
field.handleChange(Number.parseInt(value))
|
||||
field.handleChange(Number.parseInt(value, 10))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
@ -227,7 +205,7 @@ function SubscriptionCreateRouteComponent() {
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) =>
|
||||
field.handleChange(
|
||||
Number.parseInt(e.target.value)
|
||||
Number.parseInt(e.target.value, 10)
|
||||
)
|
||||
}
|
||||
placeholder={`Please enter full year (e.g. ${new Date().getFullYear()})`}
|
||||
@ -315,6 +293,29 @@ function SubscriptionCreateRouteComponent() {
|
||||
);
|
||||
}}
|
||||
</form.Subscribe>
|
||||
<form.Field name="displayName">
|
||||
{(field) => (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={field.name}>Display Name *</Label>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
placeholder="Please enter display name"
|
||||
autoComplete="off"
|
||||
/>
|
||||
{field.state.meta.errors && (
|
||||
<FormFieldErrors
|
||||
errors={field.state.meta.errors}
|
||||
isDirty={field.state.meta.isDirty}
|
||||
submissionAttempts={form.state.submissionAttempts}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</form.Field>
|
||||
<form.Field name="enabled">
|
||||
{(field) => (
|
||||
<div className="flex items-center justify-between">
|
||||
|
@ -1,3 +1,17 @@
|
||||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import {
|
||||
Edit,
|
||||
ExternalLink,
|
||||
ListIcon,
|
||||
Pause,
|
||||
Play,
|
||||
PlusIcon,
|
||||
RefreshCcwIcon,
|
||||
Trash2,
|
||||
} from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
@ -13,6 +27,7 @@ import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
||||
import { Dialog, DialogTrigger } from '@/components/ui/dialog';
|
||||
import { Img } from '@/components/ui/img';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { ProLink } from '@/components/ui/pro-link';
|
||||
import { QueryErrorView } from '@/components/ui/query-error-view';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { UPDATE_CRONS } from '@/domains/recorder/schema/cron';
|
||||
@ -33,27 +48,14 @@ import {
|
||||
FeedSourceEnum,
|
||||
FeedTypeEnum,
|
||||
type GetSubscriptionDetailQuery,
|
||||
type GetSubscriptionDetailQueryVariables,
|
||||
type InsertFeedMutation,
|
||||
type InsertFeedMutationVariables,
|
||||
SubscriptionCategoryEnum,
|
||||
type UpdateCronsMutation,
|
||||
type UpdateCronsMutationVariables,
|
||||
} from '@/infra/graphql/gql/graphql';
|
||||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import { format } from 'date-fns';
|
||||
import {
|
||||
Edit,
|
||||
ExternalLink,
|
||||
ListIcon,
|
||||
Pause,
|
||||
Play,
|
||||
PlusIcon,
|
||||
RefreshCcwIcon,
|
||||
Trash2,
|
||||
} from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { IntlService } from '@/infra/intl/intl.service';
|
||||
import { prettyTaskType } from '../tasks/-pretty-task-type';
|
||||
import { SubscriptionCronCreationDialogContent } from './-cron-creation';
|
||||
import { SubscriptionTaskCreationDialogContent } from './-task-creation';
|
||||
@ -66,6 +68,7 @@ function SubscriptionDetailRouteComponent() {
|
||||
const { id } = Route.useParams();
|
||||
const navigate = useNavigate();
|
||||
const subscriptionService = useInject(SubscriptionService);
|
||||
const intlService = useInject(IntlService);
|
||||
|
||||
const handleReload = async () => {
|
||||
const result = await refetch();
|
||||
@ -77,12 +80,23 @@ function SubscriptionDetailRouteComponent() {
|
||||
}
|
||||
};
|
||||
|
||||
const { data, loading, error, refetch } =
|
||||
useQuery<GetSubscriptionDetailQuery>(GET_SUBSCRIPTION_DETAIL, {
|
||||
const {
|
||||
data,
|
||||
loading,
|
||||
error: subscriptionError,
|
||||
refetch,
|
||||
} = useQuery<GetSubscriptionDetailQuery, GetSubscriptionDetailQueryVariables>(
|
||||
GET_SUBSCRIPTION_DETAIL,
|
||||
{
|
||||
variables: {
|
||||
id: Number.parseInt(id),
|
||||
filter: {
|
||||
id: {
|
||||
eq: Number.parseInt(id, 10),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const handleEnterEditMode = () => {
|
||||
navigate({
|
||||
@ -203,8 +217,8 @@ function SubscriptionDetailRouteComponent() {
|
||||
return <DetailCardSkeleton />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <QueryErrorView message={error.message} />;
|
||||
if (subscriptionError) {
|
||||
return <QueryErrorView message={subscriptionError.message} />;
|
||||
}
|
||||
|
||||
if (!subscription) {
|
||||
@ -342,10 +356,7 @@ function SubscriptionDetailRouteComponent() {
|
||||
<Label className="font-medium text-sm">Created at</Label>
|
||||
<div className="rounded-md bg-muted p-3">
|
||||
<span className="text-sm">
|
||||
{format(
|
||||
new Date(subscription.createdAt),
|
||||
'yyyy-MM-dd HH:mm:ss'
|
||||
)}
|
||||
{intlService.formatDatetimeWithTz(subscription.createdAt)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -354,10 +365,7 @@ function SubscriptionDetailRouteComponent() {
|
||||
<Label className="font-medium text-sm">Updated at</Label>
|
||||
<div className="rounded-md bg-muted p-3">
|
||||
<span className="text-sm">
|
||||
{format(
|
||||
new Date(subscription.updatedAt),
|
||||
'yyyy-MM-dd HH:mm:ss'
|
||||
)}
|
||||
{intlService.formatDatetimeWithTz(subscription.updatedAt)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -374,7 +382,7 @@ function SubscriptionDetailRouteComponent() {
|
||||
insertFeed({
|
||||
variables: {
|
||||
data: {
|
||||
subscriptionId: Number.parseInt(id),
|
||||
subscriptionId: Number.parseInt(id, 10),
|
||||
feedType: FeedTypeEnum.Rss,
|
||||
feedSource: FeedSourceEnum.SubscriptionEpisode,
|
||||
},
|
||||
@ -429,7 +437,7 @@ function SubscriptionDetailRouteComponent() {
|
||||
</code>
|
||||
|
||||
<div className="text-muted-foreground text-xs">
|
||||
{format(new Date(feed.createdAt), 'MM-dd HH:mm')}
|
||||
{intlService.formatDatetimeWithTz(feed.createdAt)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
@ -684,24 +692,22 @@ function SubscriptionDetailRouteComponent() {
|
||||
Updated At
|
||||
</Label>
|
||||
<div className="font-mono text-sm">
|
||||
{format(
|
||||
new Date(bangumi.updatedAt),
|
||||
'yyyy-MM-dd'
|
||||
{intlService.formatDatetimeWithTz(
|
||||
bangumi.updatedAt
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{bangumi.homepage && (
|
||||
<div className="mt-3 border-t pt-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
window.open(bangumi.homepage!, '_blank')
|
||||
}
|
||||
>
|
||||
<ExternalLink className="mr-2 h-3 w-3" />
|
||||
Homepage
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<ProLink
|
||||
href={bangumi.homepage}
|
||||
target="_blank"
|
||||
>
|
||||
<ExternalLink className="mr-2 h-3 w-3" />
|
||||
Homepage
|
||||
</ProLink>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
@ -1,3 +1,8 @@
|
||||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { Save } from 'lucide-react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
@ -36,6 +41,7 @@ import {
|
||||
apolloErrorToMessage,
|
||||
getApolloQueryError,
|
||||
} from '@/infra/errors/apollo';
|
||||
import { compatFormDefaultValues } from '@/infra/forms/compat';
|
||||
import {
|
||||
Credential3rdTypeEnum,
|
||||
type GetSubscriptionDetailQuery,
|
||||
@ -44,11 +50,6 @@ import {
|
||||
type UpdateSubscriptionsMutationVariables,
|
||||
} from '@/infra/graphql/gql/graphql';
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { Save } from 'lucide-react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { Credential3rdSelectContent } from './-credential3rd-select';
|
||||
|
||||
export const Route = createFileRoute('/_app/subscriptions/edit/$id')({
|
||||
@ -100,7 +101,9 @@ function FormView({
|
||||
category: subscription.category,
|
||||
enabled: subscription.enabled,
|
||||
sourceUrl: subscription.sourceUrl,
|
||||
credentialId: subscription.credential3rd?.id || '',
|
||||
credentialId: subscription.credential3rd?.id ?? Number.NaN,
|
||||
year: Number.NaN,
|
||||
seasonStr: '',
|
||||
};
|
||||
|
||||
if (
|
||||
@ -118,14 +121,16 @@ function FormView({
|
||||
}, [subscription, sourceUrlMeta]);
|
||||
|
||||
const form = useAppForm({
|
||||
defaultValues: defaultValues as unknown as SubscriptionForm,
|
||||
defaultValues: compatFormDefaultValues<SubscriptionForm>(defaultValues),
|
||||
validators: {
|
||||
onChangeAsync: SubscriptionFormSchema,
|
||||
onChangeAsyncDebounceMs: 300,
|
||||
onSubmit: SubscriptionFormSchema,
|
||||
},
|
||||
onSubmit: async (form) => {
|
||||
const input = subscriptionService.transformInsertFormToInput(form.value);
|
||||
onSubmit: async (submittedForm) => {
|
||||
const input = subscriptionService.transformInsertFormToInput(
|
||||
submittedForm.value
|
||||
);
|
||||
|
||||
await updateSubscription({
|
||||
variables: {
|
||||
@ -217,7 +222,7 @@ function FormView({
|
||||
<Select
|
||||
value={field.state.value.toString()}
|
||||
onValueChange={(value) =>
|
||||
field.handleChange(Number.parseInt(value))
|
||||
field.handleChange(Number.parseInt(value, 10))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
@ -249,7 +254,9 @@ function FormView({
|
||||
min={1970}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) =>
|
||||
field.handleChange(Number.parseInt(e.target.value))
|
||||
field.handleChange(
|
||||
Number.parseInt(e.target.value, 10)
|
||||
)
|
||||
}
|
||||
placeholder={`Please enter full year (e.g. ${new Date().getFullYear()})`}
|
||||
autoComplete="off"
|
||||
@ -359,7 +366,7 @@ function SubscriptionEditRouteComponent() {
|
||||
const { loading, error, data, refetch } =
|
||||
useQuery<GetSubscriptionDetailQuery>(GET_SUBSCRIPTION_DETAIL, {
|
||||
variables: {
|
||||
id: Number.parseInt(id),
|
||||
id: Number.parseInt(id, 10),
|
||||
},
|
||||
});
|
||||
|
||||
@ -367,10 +374,10 @@ function SubscriptionEditRouteComponent() {
|
||||
|
||||
const onCompleted = useCallback(async () => {
|
||||
const refetchResult = await refetch();
|
||||
const error = getApolloQueryError(refetchResult);
|
||||
if (error) {
|
||||
const _error = getApolloQueryError(refetchResult);
|
||||
if (_error) {
|
||||
toast.error('Update subscription failed', {
|
||||
description: apolloErrorToMessage(error),
|
||||
description: apolloErrorToMessage(_error),
|
||||
});
|
||||
} else {
|
||||
toast.success('Update subscription successfully');
|
||||
|
@ -1,3 +1,18 @@
|
||||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import {
|
||||
type ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
type PaginationState,
|
||||
type SortingState,
|
||||
useReactTable,
|
||||
type VisibilityState,
|
||||
} from '@tanstack/react-table';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { DataTablePagination } from '@/components/ui/data-table-pagination';
|
||||
@ -22,31 +37,19 @@ import {
|
||||
type SubscriptionDto,
|
||||
UPDATE_SUBSCRIPTIONS,
|
||||
} from '@/domains/recorder/schema/subscriptions';
|
||||
import { useInject } from '@/infra/di/inject';
|
||||
import {
|
||||
apolloErrorToMessage,
|
||||
getApolloQueryError,
|
||||
} from '@/infra/errors/apollo';
|
||||
import type { GetSubscriptionsQuery } from '@/infra/graphql/gql/graphql';
|
||||
import type {
|
||||
GetSubscriptionsQuery,
|
||||
GetSubscriptionsQueryVariables,
|
||||
} from '@/infra/graphql/gql/graphql';
|
||||
import { IntlService } from '@/infra/intl/intl.service';
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { useDebouncedSkeleton } from '@/presentation/hooks/use-debounded-skeleton';
|
||||
import { cn } from '@/presentation/utils';
|
||||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import {
|
||||
type ColumnDef,
|
||||
type PaginationState,
|
||||
type SortingState,
|
||||
type VisibilityState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
import { format } from 'date-fns';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { SubscriptionTaskCreationDialogContent } from './-task-creation';
|
||||
|
||||
export const Route = createFileRoute('/_app/subscriptions/manage')({
|
||||
@ -58,6 +61,7 @@ export const Route = createFileRoute('/_app/subscriptions/manage')({
|
||||
|
||||
function SubscriptionManageRouteComponent() {
|
||||
const navigate = useNavigate();
|
||||
const intlService = useInject(IntlService);
|
||||
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({
|
||||
createdAt: false,
|
||||
@ -69,7 +73,12 @@ function SubscriptionManageRouteComponent() {
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const { loading, error, data, refetch } = useQuery<GetSubscriptionsQuery>(
|
||||
const {
|
||||
loading,
|
||||
error: subscriptionsError,
|
||||
data,
|
||||
refetch,
|
||||
} = useQuery<GetSubscriptionsQuery, GetSubscriptionsQueryVariables>(
|
||||
GET_SUBSCRIPTIONS,
|
||||
{
|
||||
variables: {
|
||||
@ -138,11 +147,11 @@ function SubscriptionManageRouteComponent() {
|
||||
<div className="px-1">
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onCheckedChange={(enabled) =>
|
||||
onCheckedChange={(checked) =>
|
||||
updateSubscription({
|
||||
variables: {
|
||||
data: {
|
||||
enabled,
|
||||
enabled: checked,
|
||||
},
|
||||
filter: {
|
||||
id: {
|
||||
@ -189,7 +198,7 @@ function SubscriptionManageRouteComponent() {
|
||||
const createdAt = row.original.createdAt;
|
||||
return (
|
||||
<div className="text-sm">
|
||||
{format(new Date(createdAt), 'yyyy-MM-dd HH:mm:ss')}
|
||||
{intlService.formatDatetimeWithTz(createdAt)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@ -201,7 +210,7 @@ function SubscriptionManageRouteComponent() {
|
||||
const updatedAt = row.original.updatedAt;
|
||||
return (
|
||||
<div className="text-sm">
|
||||
{format(new Date(updatedAt), 'yyyy-MM-dd HH:mm:ss')}
|
||||
{intlService.formatDatetimeWithTz(updatedAt)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@ -247,7 +256,12 @@ function SubscriptionManageRouteComponent() {
|
||||
},
|
||||
];
|
||||
return cs;
|
||||
}, [updateSubscription, deleteSubscription, navigate]);
|
||||
}, [
|
||||
updateSubscription,
|
||||
deleteSubscription,
|
||||
navigate,
|
||||
intlService.formatDatetimeWithTz,
|
||||
]);
|
||||
|
||||
const table = useReactTable({
|
||||
data: useMemo(() => subscriptions?.nodes ?? [], [subscriptions]),
|
||||
@ -274,8 +288,10 @@ function SubscriptionManageRouteComponent() {
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return <QueryErrorView message={error.message} onRetry={refetch} />;
|
||||
if (subscriptionsError) {
|
||||
return (
|
||||
<QueryErrorView message={subscriptionsError.message} onRetry={refetch} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -1,9 +1,314 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
import { useMemo } 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 { 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';
|
||||
|
||||
export const Route = createFileRoute('/_app/tasks/cron/detail/$id')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
component: CronDetailRouteComponent,
|
||||
staticData: {
|
||||
breadcrumb: { label: 'Detail' },
|
||||
} satisfies RouteStateDataOption,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/_app/tasks/cron/detail/$id"!</div>
|
||||
function CronDetailRouteComponent() {
|
||||
const { id } = Route.useParams();
|
||||
const intlService = useInject(IntlService);
|
||||
|
||||
const { data, loading, error, refetch } = useQuery<
|
||||
GetCronsQuery,
|
||||
GetCronsQueryVariables
|
||||
>(GET_CRONS, {
|
||||
variables: {
|
||||
filter: {
|
||||
id: {
|
||||
eq: Number.parseInt(id, 10),
|
||||
},
|
||||
},
|
||||
pagination: {
|
||||
page: {
|
||||
page: 0,
|
||||
limit: 1,
|
||||
},
|
||||
},
|
||||
orderBy: {},
|
||||
},
|
||||
pollInterval: 5000, // Auto-refresh every 5 seconds for running crons
|
||||
});
|
||||
|
||||
const cron = data?.cron?.nodes?.[0];
|
||||
|
||||
const subscriberTaskCron = useMemo(() => {
|
||||
if (!cron) {
|
||||
return null;
|
||||
}
|
||||
return cron.subscriberTaskCron;
|
||||
}, [cron]);
|
||||
|
||||
if (loading) {
|
||||
return <DetailCardSkeleton />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <QueryErrorView message={error.message} onRetry={refetch} />;
|
||||
}
|
||||
|
||||
if (!cron) {
|
||||
return <DetailEmptyView message="Not found Cron task" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-4xl py-6">
|
||||
<ContainerHeader
|
||||
title="Cron task detail"
|
||||
description={`View Cron task #${cron.id}`}
|
||||
defaultBackTo="/tasks/cron/manage"
|
||||
actions={
|
||||
<Button variant="outline" size="sm" onClick={() => refetch()}>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Refresh
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Cron task information</CardTitle>
|
||||
<CardDescription className="mt-2">
|
||||
View Cron task execution details
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{getStatusBadge(cron.status)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
{/* 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">ID</Label>
|
||||
<div className="rounded-md bg-muted p-3">
|
||||
<code className="text-sm">{cron.id}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="font-medium text-sm">Priority</Label>
|
||||
<div className="rounded-md bg-muted p-3">
|
||||
<span className="text-sm">{cron.priority}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="font-medium text-sm">Attemps</Label>
|
||||
<div className="rounded-md bg-muted p-3">
|
||||
<span className="text-sm">
|
||||
{cron.attempts} / {cron.maxAttempts}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="font-medium text-sm">Enabled</Label>
|
||||
<div className="rounded-md bg-muted p-3">
|
||||
<Badge variant={cron.enabled ? 'default' : 'secondary'}>
|
||||
{cron.enabled ? 'Enabled' : 'Disabled'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="font-medium text-sm">Next run time</Label>
|
||||
<div className="rounded-md bg-muted p-3">
|
||||
<span className="text-sm">
|
||||
{cron.nextRun
|
||||
? intlService.formatDatetimeWithTz(cron.nextRun)
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="font-medium text-sm">Last run time</Label>
|
||||
<div className="rounded-md bg-muted p-3">
|
||||
<span className="text-sm">
|
||||
{cron.lastRun
|
||||
? intlService.formatDatetimeWithTz(cron.lastRun)
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="font-medium text-sm">Locked time</Label>
|
||||
<div className="rounded-md bg-muted p-3">
|
||||
<span className="text-sm">
|
||||
{cron.lockedAt
|
||||
? intlService.formatDatetimeWithTz(cron.lockedAt)
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="font-medium text-sm">Locked by</Label>
|
||||
<div className="rounded-md bg-muted p-3">
|
||||
<code className="text-sm">{cron.lockedBy || '-'}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="font-medium text-sm">Timeout</Label>
|
||||
<div className="rounded-md bg-muted p-3">
|
||||
<span className="text-sm">
|
||||
{cron.timeoutMs ? `${cron.timeoutMs}ms` : 'No limit'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="font-medium text-sm">Created at</Label>
|
||||
<div className="rounded-md bg-muted p-3">
|
||||
<span className="text-sm">
|
||||
{intlService.formatDatetimeWithTz(cron.createdAt)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="font-medium text-sm">Updated at</Label>
|
||||
<div className="rounded-md bg-muted p-3">
|
||||
<span className="text-sm">
|
||||
{intlService.formatDatetimeWithTz(cron.updatedAt)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Subscriber Task Details */}
|
||||
{subscriberTaskCron && (
|
||||
<>
|
||||
<Separator />
|
||||
<div className="space-y-2">
|
||||
<Label className="font-medium text-sm">
|
||||
Subscriber task details
|
||||
</Label>
|
||||
<div className="rounded-md bg-muted p-3">
|
||||
<pre className="overflow-x-auto whitespace-pre-wrap text-sm">
|
||||
<code>
|
||||
{JSON.stringify(subscriberTaskCron, null, 2)}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Cron Expression Display */}
|
||||
{cron.cronExpr && (
|
||||
<>
|
||||
<Separator />
|
||||
<div className="space-y-2">
|
||||
<Label className="font-medium text-sm">
|
||||
Cron expression
|
||||
</Label>
|
||||
<CronDisplay
|
||||
expression={cron.cronExpr}
|
||||
timezone="UTC"
|
||||
showDescription={true}
|
||||
showNextRuns={true}
|
||||
withCard={false}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Related Subscriber Tasks */}
|
||||
{cron.subscriberTask?.nodes &&
|
||||
cron.subscriberTask.nodes.length > 0 && (
|
||||
<>
|
||||
<Separator />
|
||||
<div className="space-y-2">
|
||||
<Label className="font-medium text-sm">
|
||||
Associated tasks
|
||||
</Label>
|
||||
<div className="space-y-2">
|
||||
{cron.subscriberTask.nodes.map((task, index) => (
|
||||
<div
|
||||
key={`${task.id}-${index}`}
|
||||
className="rounded-md border bg-muted/50 p-3"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<code className="text-sm">{task.id}</code>
|
||||
<Badge variant="outline">{task.status}</Badge>
|
||||
</div>
|
||||
<div className="mt-2 text-muted-foreground text-sm">
|
||||
Priority: {task.priority} | Attempts:{' '}
|
||||
{task.attempts}/{task.maxAttempts}
|
||||
</div>
|
||||
{task.subscription && (
|
||||
<div className="mt-2 text-sm">
|
||||
<span className="font-medium">
|
||||
Subscription:
|
||||
</span>{' '}
|
||||
{task.subscription.displayName}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Error Information */}
|
||||
{cron.status === CronStatusEnum.Failed && cron.lastError && (
|
||||
<>
|
||||
<Separator />
|
||||
<div className="space-y-2">
|
||||
<Label className="font-medium text-sm">最后错误</Label>
|
||||
<div className="rounded-md bg-destructive/10 p-3">
|
||||
<p className="text-destructive text-sm">
|
||||
{cron.lastError}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,9 +1,22 @@
|
||||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import {
|
||||
type ColumnDef,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
type PaginationState,
|
||||
type SortingState,
|
||||
useReactTable,
|
||||
type VisibilityState,
|
||||
} from '@tanstack/react-table';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { DataTablePagination } from '@/components/ui/data-table-pagination';
|
||||
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
||||
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
||||
import { DropdownMenuActions } from '@/components/ui/dropdown-menu-actions';
|
||||
import { QueryErrorView } from '@/components/ui/query-error-view';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
@ -12,6 +25,7 @@ import {
|
||||
DELETE_CRONS,
|
||||
GET_CRONS,
|
||||
} from '@/domains/recorder/schema/cron';
|
||||
import { useInject } from '@/infra/di/inject';
|
||||
import {
|
||||
apolloErrorToMessage,
|
||||
getApolloQueryError,
|
||||
@ -23,23 +37,9 @@ import {
|
||||
type GetCronsQuery,
|
||||
type GetCronsQueryVariables,
|
||||
} from '@/infra/graphql/gql/graphql';
|
||||
import { IntlService } from '@/infra/intl/intl.service';
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { useDebouncedSkeleton } from '@/presentation/hooks/use-debounded-skeleton';
|
||||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import {
|
||||
type ColumnDef,
|
||||
type PaginationState,
|
||||
type SortingState,
|
||||
type VisibilityState,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
import { format } from 'date-fns';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { getStatusBadge } from './-status-badge';
|
||||
|
||||
export const Route = createFileRoute('/_app/tasks/cron/manage')({
|
||||
@ -51,6 +51,7 @@ export const Route = createFileRoute('/_app/tasks/cron/manage')({
|
||||
|
||||
function TaskCronManageRouteComponent() {
|
||||
const navigate = useNavigate();
|
||||
const intlService = useInject(IntlService);
|
||||
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
@ -88,18 +89,18 @@ function TaskCronManageRouteComponent() {
|
||||
>(DELETE_CRONS, {
|
||||
onCompleted: async () => {
|
||||
const refetchResult = await refetch();
|
||||
const error = getApolloQueryError(refetchResult);
|
||||
if (error) {
|
||||
const errorResult = getApolloQueryError(refetchResult);
|
||||
if (errorResult) {
|
||||
toast.error('Failed to delete tasks', {
|
||||
description: apolloErrorToMessage(error),
|
||||
description: apolloErrorToMessage(errorResult),
|
||||
});
|
||||
return;
|
||||
}
|
||||
toast.success('Tasks deleted');
|
||||
},
|
||||
onError: (error) => {
|
||||
onError: (mutationError) => {
|
||||
toast.error('Failed to delete tasks', {
|
||||
description: error.message,
|
||||
description: mutationError.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
@ -168,16 +169,16 @@ function TaskCronManageRouteComponent() {
|
||||
<div className="space-y-3">
|
||||
{showSkeleton &&
|
||||
Array.from(new Array(10)).map((_, index) => (
|
||||
<Skeleton key={index} className="h-32 w-full" />
|
||||
<Skeleton key={`skeleton-${index}`} className="h-32 w-full" />
|
||||
))}
|
||||
|
||||
{!showSkeleton && table.getRowModel().rows?.length > 0 ? (
|
||||
table.getRowModel().rows.map((row, index) => {
|
||||
table.getRowModel().rows.map((row) => {
|
||||
const cron = row.original;
|
||||
return (
|
||||
<div
|
||||
className="space-y-3 rounded-lg border bg-card p-4"
|
||||
key={`${cron.id}-${index}`}
|
||||
key={cron.id}
|
||||
>
|
||||
{/* Header with status and priority */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
@ -215,17 +216,7 @@ function TaskCronManageRouteComponent() {
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
{cron.status === CronStatusEnum.Failed && (
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
// TODO: Retry cron
|
||||
}}
|
||||
>
|
||||
Retry
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuActions>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -235,7 +226,7 @@ function TaskCronManageRouteComponent() {
|
||||
<span className="text-muted-foreground">Next run: </span>
|
||||
<span>
|
||||
{cron.nextRun
|
||||
? format(new Date(cron.nextRun), 'MM/dd HH:mm')
|
||||
? intlService.formatDatetimeWithTz(cron.nextRun)
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
@ -244,7 +235,7 @@ function TaskCronManageRouteComponent() {
|
||||
<span className="text-muted-foreground">Last run: </span>
|
||||
<span>
|
||||
{cron.lastRun
|
||||
? format(new Date(cron.lastRun), 'MM/dd HH:mm')
|
||||
? intlService.formatDatetimeWithTz(cron.lastRun)
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
@ -262,7 +253,7 @@ function TaskCronManageRouteComponent() {
|
||||
<span className="text-muted-foreground">Lock at: </span>
|
||||
<span>
|
||||
{cron.lockedAt
|
||||
? format(new Date(cron.lockedAt), 'MM/dd HH:mm')
|
||||
? intlService.formatDatetimeWithTz(cron.lockedAt)
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
|
@ -1,3 +1,8 @@
|
||||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
@ -14,8 +19,11 @@ import { Label } from '@/components/ui/label';
|
||||
import { QueryErrorView } from '@/components/ui/query-error-view';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { GET_TASKS, RETRY_TASKS } from '@/domains/recorder/schema/tasks';
|
||||
import { getApolloQueryError } from '@/infra/errors/apollo';
|
||||
import { apolloErrorToMessage } from '@/infra/errors/apollo';
|
||||
import { useInject } from '@/infra/di/inject';
|
||||
import {
|
||||
apolloErrorToMessage,
|
||||
getApolloQueryError,
|
||||
} from '@/infra/errors/apollo';
|
||||
import {
|
||||
type GetTasksQuery,
|
||||
type GetTasksQueryVariables,
|
||||
@ -23,13 +31,8 @@ import {
|
||||
type RetryTasksMutationVariables,
|
||||
SubscriberTaskStatusEnum,
|
||||
} from '@/infra/graphql/gql/graphql';
|
||||
import { IntlService } from '@/infra/intl/intl.service';
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { format } from 'date-fns';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { prettyTaskType } from './-pretty-task-type';
|
||||
import { getStatusBadge } from './-status-badge';
|
||||
|
||||
@ -43,10 +46,14 @@ export const Route = createFileRoute('/_app/tasks/detail/$id')({
|
||||
function TaskDetailRouteComponent() {
|
||||
const { id } = Route.useParams();
|
||||
|
||||
const { data, loading, error, refetch } = useQuery<
|
||||
GetTasksQuery,
|
||||
GetTasksQueryVariables
|
||||
>(GET_TASKS, {
|
||||
const intlService = useInject(IntlService);
|
||||
|
||||
const {
|
||||
data,
|
||||
loading,
|
||||
error: taskError,
|
||||
refetch,
|
||||
} = useQuery<GetTasksQuery, GetTasksQueryVariables>(GET_TASKS, {
|
||||
variables: {
|
||||
filter: {
|
||||
id: {
|
||||
@ -102,8 +109,8 @@ function TaskDetailRouteComponent() {
|
||||
return <DetailCardSkeleton />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <QueryErrorView message={error.message} onRetry={refetch} />;
|
||||
if (taskError) {
|
||||
return <QueryErrorView message={taskError.message} onRetry={refetch} />;
|
||||
}
|
||||
|
||||
if (!task) {
|
||||
@ -195,7 +202,7 @@ function TaskDetailRouteComponent() {
|
||||
</Label>
|
||||
<div className="rounded-md bg-muted p-3">
|
||||
<span className="text-sm">
|
||||
{format(new Date(task.runAt), 'yyyy-MM-dd HH:mm:ss')}
|
||||
{intlService.formatDatetimeWithTz(task.runAt)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -205,7 +212,7 @@ function TaskDetailRouteComponent() {
|
||||
<div className="rounded-md bg-muted p-3">
|
||||
<span className="text-sm">
|
||||
{task.doneAt
|
||||
? format(new Date(task.doneAt), 'yyyy-MM-dd HH:mm:ss')
|
||||
? intlService.formatDatetimeWithTz(task.doneAt)
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
@ -216,7 +223,7 @@ function TaskDetailRouteComponent() {
|
||||
<div className="rounded-md bg-muted p-3">
|
||||
<span className="text-sm">
|
||||
{task.lockAt
|
||||
? format(new Date(task.lockAt), 'yyyy-MM-dd HH:mm:ss')
|
||||
? intlService.formatDatetimeWithTz(task.lockAt)
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
|
@ -1,7 +1,23 @@
|
||||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import {
|
||||
type ColumnDef,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
type PaginationState,
|
||||
type SortingState,
|
||||
useReactTable,
|
||||
type VisibilityState,
|
||||
} from '@tanstack/react-table';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { DataTablePagination } from '@/components/ui/data-table-pagination';
|
||||
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
||||
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
||||
import { DropdownMenuActions } from '@/components/ui/dropdown-menu-actions';
|
||||
import { QueryErrorView } from '@/components/ui/query-error-view';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
@ -11,6 +27,11 @@ import {
|
||||
RETRY_TASKS,
|
||||
type TaskDto,
|
||||
} from '@/domains/recorder/schema/tasks';
|
||||
import { useInject } from '@/infra/di/inject';
|
||||
import {
|
||||
apolloErrorToMessage,
|
||||
getApolloQueryError,
|
||||
} from '@/infra/errors/apollo';
|
||||
import {
|
||||
type DeleteTasksMutation,
|
||||
type DeleteTasksMutationVariables,
|
||||
@ -20,30 +41,9 @@ import {
|
||||
type RetryTasksMutationVariables,
|
||||
SubscriberTaskStatusEnum,
|
||||
} from '@/infra/graphql/gql/graphql';
|
||||
import { IntlService } from '@/infra/intl/intl.service';
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { useDebouncedSkeleton } from '@/presentation/hooks/use-debounded-skeleton';
|
||||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import {
|
||||
type ColumnDef,
|
||||
type PaginationState,
|
||||
type SortingState,
|
||||
type VisibilityState,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
import { format } from 'date-fns';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
apolloErrorToMessage,
|
||||
getApolloQueryError,
|
||||
} from '@/infra/errors/apollo';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { prettyTaskType } from './-pretty-task-type';
|
||||
import { getStatusBadge } from './-status-badge';
|
||||
|
||||
@ -64,10 +64,14 @@ function TaskManageRouteComponent() {
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const { loading, error, data, refetch } = useQuery<
|
||||
GetTasksQuery,
|
||||
GetTasksQueryVariables
|
||||
>(GET_TASKS, {
|
||||
const intlService = useInject(IntlService);
|
||||
|
||||
const {
|
||||
loading,
|
||||
error: tasksError,
|
||||
data,
|
||||
refetch,
|
||||
} = useQuery<GetTasksQuery, GetTasksQueryVariables>(GET_TASKS, {
|
||||
variables: {
|
||||
pagination: {
|
||||
page: {
|
||||
@ -168,8 +172,8 @@ function TaskManageRouteComponent() {
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return <QueryErrorView message={error.message} onRetry={refetch} />;
|
||||
if (tasksError) {
|
||||
return <QueryErrorView message={tasksError.message} onRetry={refetch} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@ -262,14 +266,14 @@ function TaskManageRouteComponent() {
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div>
|
||||
<span className="text-muted-foreground">Run at: </span>
|
||||
<span>{format(new Date(task.runAt), 'MM/dd HH:mm')}</span>
|
||||
<span>{intlService.formatDatetimeWithTz(task.runAt)}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="text-muted-foreground">Done: </span>
|
||||
<span>
|
||||
{task.doneAt
|
||||
? format(new Date(task.doneAt), 'MM/dd HH:mm')
|
||||
? intlService.formatDatetimeWithTz(task.doneAt)
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
@ -287,7 +291,7 @@ function TaskManageRouteComponent() {
|
||||
<span className="text-muted-foreground">Lock at: </span>
|
||||
<span>
|
||||
{task.lockAt
|
||||
? format(new Date(task.lockAt), 'MM/dd HH:mm')
|
||||
? intlService.formatDatetimeWithTz(task.lockAt)
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
|
50
biome.json
50
biome.json
@ -1,31 +1,39 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/2.1.1/schema.json",
|
||||
"extends": ["ultracite"],
|
||||
"javascript": {
|
||||
"globals": ["Liveblocks"]
|
||||
},
|
||||
|
||||
"assist": {
|
||||
"actions": {
|
||||
"source": {
|
||||
"useSortedAttributes": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"linter": {
|
||||
"rules": {
|
||||
"nursery": {
|
||||
"noEnum": "off"
|
||||
},
|
||||
"nursery": {},
|
||||
"style": {
|
||||
"noParameterProperties": "off",
|
||||
"noNonNullAssertion": "off"
|
||||
"noNonNullAssertion": "off",
|
||||
"noEnum": "off"
|
||||
},
|
||||
"security": {
|
||||
"noDangerouslySetInnerHtml": "off"
|
||||
},
|
||||
"suspicious": {
|
||||
"noArrayIndexKey": "off",
|
||||
"noEmptyBlockStatements": "off",
|
||||
"noExplicitAny": "off",
|
||||
"noConsole": "off",
|
||||
"noConsoleLog": "off"
|
||||
"noConsole": "off"
|
||||
},
|
||||
"a11y": {
|
||||
"noSvgWithoutTitle": "off"
|
||||
},
|
||||
"performance": {
|
||||
"noNamespaceImport": "off"
|
||||
},
|
||||
"complexity": {
|
||||
"noExcessiveCognitiveComplexity": {
|
||||
"level": "warn",
|
||||
@ -36,16 +44,20 @@
|
||||
"noBannedTypes": "off"
|
||||
},
|
||||
"correctness": {
|
||||
"noUnusedVariables": {
|
||||
"fix": "none",
|
||||
"level": "error"
|
||||
},
|
||||
"noUnusedImports": {
|
||||
"fix": "none",
|
||||
"level": "warn"
|
||||
"level": "error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"include": ["**/tsconfig.json", "**/tsconfig.*.json"],
|
||||
"includes": ["**/tsconfig.json", "**/tsconfig.*.json"],
|
||||
"json": {
|
||||
"parser": {
|
||||
"allowComments": true
|
||||
@ -53,11 +65,17 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"include": ["apps/webui/src/infra/graphql/gql/**/*"],
|
||||
"includes": ["**/apps/webui/src/infra/graphql/gql/**/*"],
|
||||
"assist": {
|
||||
"actions": {
|
||||
"source": {
|
||||
"organizeImports": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"linter": {
|
||||
"rules": {
|
||||
"style": {
|
||||
"useShorthandArrayType": "off",
|
||||
"useConsistentArrayType": "off",
|
||||
"useImportType": "off"
|
||||
}
|
||||
@ -65,7 +83,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"include": ["apps/webui/src/components/ui/**/*"],
|
||||
"includes": ["**/apps/webui/src/components/ui/**/*"],
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "double"
|
||||
@ -75,10 +93,10 @@
|
||||
"rules": {
|
||||
"style": {
|
||||
"useBlockStatements": "off",
|
||||
"useImportType": "off"
|
||||
"useImportType": "off",
|
||||
"noNestedTernary": "off"
|
||||
},
|
||||
"nursery": {
|
||||
"noNestedTernary": "off",
|
||||
"useSortedClasses": "off"
|
||||
},
|
||||
"a11y": {
|
||||
@ -94,6 +112,6 @@
|
||||
}
|
||||
],
|
||||
"files": {
|
||||
"ignore": [".vscode/*.json"]
|
||||
"includes": ["**", "!**/.vscode/**/*.json"]
|
||||
}
|
||||
}
|
||||
|
@ -18,22 +18,23 @@
|
||||
"bump-deps": "npx --yes npm-check-updates --deep -u && pnpm install",
|
||||
"clean": "git clean -xdf node_modules"
|
||||
},
|
||||
"packageManager": "pnpm@10.12.1",
|
||||
"packageManager": "pnpm@10.12.4",
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
"node": ">=24"
|
||||
},
|
||||
"dependencies": {
|
||||
"@typescript/native-preview": "7.0.0-dev.20250712.1",
|
||||
"es-toolkit": "^1.39.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@biomejs/biome": "2.1.1",
|
||||
"@types/node": "^24.0.10",
|
||||
"cross-env": "^7.0.3",
|
||||
"kill-port": "^2.0.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"tsx": "^4.20.3",
|
||||
"typescript": "^5.8.3",
|
||||
"ultracite": "^4.2.13"
|
||||
"ultracite": "^5.0.32"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
|
385
pnpm-lock.yaml
generated
385
pnpm-lock.yaml
generated
@ -11,13 +11,16 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@typescript/native-preview':
|
||||
specifier: 7.0.0-dev.20250712.1
|
||||
version: 7.0.0-dev.20250712.1
|
||||
es-toolkit:
|
||||
specifier: ^1.39.6
|
||||
version: 1.39.6
|
||||
devDependencies:
|
||||
'@biomejs/biome':
|
||||
specifier: 1.9.4
|
||||
version: 1.9.4
|
||||
specifier: 2.1.1
|
||||
version: 2.1.1
|
||||
'@types/node':
|
||||
specifier: ^24.0.10
|
||||
version: 24.0.10
|
||||
@ -37,8 +40,8 @@ importers:
|
||||
specifier: ^5.8.3
|
||||
version: 5.8.3
|
||||
ultracite:
|
||||
specifier: ^4.2.13
|
||||
version: 4.2.13
|
||||
specifier: ^5.0.32
|
||||
version: 5.0.32(@types/node@24.0.10)(jsdom@25.0.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1)
|
||||
|
||||
apps/docs: {}
|
||||
|
||||
@ -601,59 +604,65 @@ packages:
|
||||
resolution: {integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@biomejs/biome@1.9.4':
|
||||
resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==}
|
||||
'@biomejs/biome@2.1.1':
|
||||
resolution: {integrity: sha512-HFGYkxG714KzG+8tvtXCJ1t1qXQMzgWzfvQaUjxN6UeKv+KvMEuliInnbZLJm6DXFXwqVi6446EGI0sGBLIYng==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
hasBin: true
|
||||
|
||||
'@biomejs/cli-darwin-arm64@1.9.4':
|
||||
resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==}
|
||||
'@biomejs/cli-darwin-arm64@2.1.1':
|
||||
resolution: {integrity: sha512-2Muinu5ok4tWxq4nu5l19el48cwCY/vzvI7Vjbkf3CYIQkjxZLyj0Ad37Jv2OtlXYaLvv+Sfu1hFeXt/JwRRXQ==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@biomejs/cli-darwin-x64@1.9.4':
|
||||
resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==}
|
||||
'@biomejs/cli-darwin-x64@2.1.1':
|
||||
resolution: {integrity: sha512-cC8HM5lrgKQXLAK+6Iz2FrYW5A62pAAX6KAnRlEyLb+Q3+Kr6ur/sSuoIacqlp1yvmjHJqjYfZjPvHWnqxoEIA==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@biomejs/cli-linux-arm64-musl@1.9.4':
|
||||
resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==}
|
||||
'@biomejs/cli-linux-arm64-musl@2.1.1':
|
||||
resolution: {integrity: sha512-/7FBLnTswu4jgV9ttI3AMIdDGqVEPIZd8I5u2D4tfCoj8rl9dnjrEQbAIDlWhUXdyWlFSz8JypH3swU9h9P+2A==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@biomejs/cli-linux-arm64@1.9.4':
|
||||
resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==}
|
||||
'@biomejs/cli-linux-arm64@2.1.1':
|
||||
resolution: {integrity: sha512-tw4BEbhAUkWPe4WBr6IX04DJo+2jz5qpPzpW/SWvqMjb9QuHY8+J0M23V8EPY/zWU4IG8Ui0XESapR1CB49Q7g==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@biomejs/cli-linux-x64-musl@1.9.4':
|
||||
resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==}
|
||||
'@biomejs/cli-linux-x64-musl@2.1.1':
|
||||
resolution: {integrity: sha512-kUu+loNI3OCD2c12cUt7M5yaaSjDnGIksZwKnueubX6c/HWUyi/0mPbTBHR49Me3F0KKjWiKM+ZOjsmC+lUt9g==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@biomejs/cli-linux-x64@1.9.4':
|
||||
resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==}
|
||||
'@biomejs/cli-linux-x64@2.1.1':
|
||||
resolution: {integrity: sha512-3WJ1GKjU7NzZb6RTbwLB59v9cTIlzjbiFLDB0z4376TkDqoNYilJaC37IomCr/aXwuU8QKkrYoHrgpSq5ffJ4Q==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@biomejs/cli-win32-arm64@1.9.4':
|
||||
resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==}
|
||||
'@biomejs/cli-win32-arm64@2.1.1':
|
||||
resolution: {integrity: sha512-vEHK0v0oW+E6RUWLoxb2isI3rZo57OX9ZNyyGH701fZPj6Il0Rn1f5DMNyCmyflMwTnIQstEbs7n2BxYSqQx4Q==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@biomejs/cli-win32-x64@1.9.4':
|
||||
resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==}
|
||||
'@biomejs/cli-win32-x64@2.1.1':
|
||||
resolution: {integrity: sha512-i2PKdn70kY++KEF/zkQFvQfX1e8SkA8hq4BgC+yE9dZqyLzB/XStY2MvwI3qswlRgnGpgncgqe0QYKVS1blksg==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@clack/core@0.5.0':
|
||||
resolution: {integrity: sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==}
|
||||
|
||||
'@clack/prompts@0.11.0':
|
||||
resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==}
|
||||
|
||||
'@codemirror/language@6.11.1':
|
||||
resolution: {integrity: sha512-5kS1U7emOGV84vxC+ruBty5sUgcD0te6dyupyRVG2zaSjhTDM73LhVKUtVwiqSe6QwmEoA4SCiU8AKPFyumAWQ==}
|
||||
|
||||
@ -3202,6 +3211,53 @@ packages:
|
||||
'@types/ws@8.18.1':
|
||||
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
|
||||
|
||||
'@typescript/native-preview-darwin-arm64@7.0.0-dev.20250712.1':
|
||||
resolution: {integrity: sha512-h5MlpLXr6I6zrKZKZOjyKdUhzUuO2+kKLEYmrR0HWS/U6dlcwvNuS2wUo1lRNgUTCblHJuOKmyWx3Sz+JG8Oxw==}
|
||||
engines: {node: '>=20.6.0'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@typescript/native-preview-darwin-x64@7.0.0-dev.20250712.1':
|
||||
resolution: {integrity: sha512-72SPfl0/U2ra4KlkzLWgA86zLKUIimBPYE1NqZpJHLMoXkC5XtS9aXT8p6ivxkK+p1VV3fIWKM8BbxOUY3af0A==}
|
||||
engines: {node: '>=20.6.0'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@typescript/native-preview-linux-arm64@7.0.0-dev.20250712.1':
|
||||
resolution: {integrity: sha512-LqIRXAn1xC5amD+ypd7xVTyMhgbbhb9XLzLM32Gr8ogJUCvcLBbd8KCHsKnHSR8nc+3b9FKnYdK7YL6a2IqavA==}
|
||||
engines: {node: '>=20.6.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/native-preview-linux-arm@7.0.0-dev.20250712.1':
|
||||
resolution: {integrity: sha512-XedHV/oRfLKrfU7XE5/Fz8Lf+eKCaQyNINCikQQlAuQWgT4pf8gW9FPkZYmeNvl7y+++43Wr2YJklmwXMrIDiA==}
|
||||
engines: {node: '>=20.6.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/native-preview-linux-x64@7.0.0-dev.20250712.1':
|
||||
resolution: {integrity: sha512-dCIb4GTXvetTpeVJRsKf0lNnq1udfjkhDmyUX15yWV41mPg+PJUiLeZty2GOwFovSfEUSK+pQSP2iaA6ITbLtw==}
|
||||
engines: {node: '>=20.6.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/native-preview-win32-arm64@7.0.0-dev.20250712.1':
|
||||
resolution: {integrity: sha512-+Wze9OFlre7YxOh/2LePfh6TmdwMJkSyiFD6XRtmm1hkwoB8jk5h1Q5aC5P4a3LTYx6jie6eYSVSYUXtaWZqMw==}
|
||||
engines: {node: '>=20.6.0'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@typescript/native-preview-win32-x64@7.0.0-dev.20250712.1':
|
||||
resolution: {integrity: sha512-PwLTlosngLgI4O41qjIFFanl5Q+G8bzUIvFdT0yk2vPAPnPyT0N2gLmGbAJouM3nSe7e+id2+iqdY+QCPvh0uA==}
|
||||
engines: {node: '>=20.6.0'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@typescript/native-preview@7.0.0-dev.20250712.1':
|
||||
resolution: {integrity: sha512-A8/aOsMpG6H8IcSIKYJSuHzbNkVr8dJOxbb4LMrSfOZ/JWayHQ4O5UJ9mSaKtyPwR6fInE5B8yMt7BYQOz77kA==}
|
||||
engines: {node: '>=20.6.0'}
|
||||
hasBin: true
|
||||
|
||||
'@vitejs/plugin-react@4.5.2':
|
||||
resolution: {integrity: sha512-QNVT3/Lxx99nMQWJWF7K4N6apUEuT0KlZA3mx/mVaoGj3smm/8rc8ezz15J1pcbcjDK0V15rpHetVfya08r76Q==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
@ -3211,6 +3267,9 @@ packages:
|
||||
'@vitest/expect@3.2.3':
|
||||
resolution: {integrity: sha512-W2RH2TPWVHA1o7UmaFKISPvdicFJH+mjykctJFoAkUw+SPTJTGjUNdKscFBrqM7IPnCVu6zihtKYa7TkZS1dkQ==}
|
||||
|
||||
'@vitest/expect@3.2.4':
|
||||
resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==}
|
||||
|
||||
'@vitest/mocker@3.2.3':
|
||||
resolution: {integrity: sha512-cP6fIun+Zx8he4rbWvi+Oya6goKQDZK+Yq4hhlggwQBbrlOQ4qtZ+G4nxB6ZnzI9lyIb+JnvyiJnPC2AGbKSPA==}
|
||||
peerDependencies:
|
||||
@ -3222,21 +3281,47 @@ packages:
|
||||
vite:
|
||||
optional: true
|
||||
|
||||
'@vitest/mocker@3.2.4':
|
||||
resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==}
|
||||
peerDependencies:
|
||||
msw: ^2.4.9
|
||||
vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0
|
||||
peerDependenciesMeta:
|
||||
msw:
|
||||
optional: true
|
||||
vite:
|
||||
optional: true
|
||||
|
||||
'@vitest/pretty-format@3.2.3':
|
||||
resolution: {integrity: sha512-yFglXGkr9hW/yEXngO+IKMhP0jxyFw2/qys/CK4fFUZnSltD+MU7dVYGrH8rvPcK/O6feXQA+EU33gjaBBbAng==}
|
||||
|
||||
'@vitest/pretty-format@3.2.4':
|
||||
resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==}
|
||||
|
||||
'@vitest/runner@3.2.3':
|
||||
resolution: {integrity: sha512-83HWYisT3IpMaU9LN+VN+/nLHVBCSIUKJzGxC5RWUOsK1h3USg7ojL+UXQR3b4o4UBIWCYdD2fxuzM7PQQ1u8w==}
|
||||
|
||||
'@vitest/runner@3.2.4':
|
||||
resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==}
|
||||
|
||||
'@vitest/snapshot@3.2.3':
|
||||
resolution: {integrity: sha512-9gIVWx2+tysDqUmmM1L0hwadyumqssOL1r8KJipwLx5JVYyxvVRfxvMq7DaWbZZsCqZnu/dZedaZQh4iYTtneA==}
|
||||
|
||||
'@vitest/snapshot@3.2.4':
|
||||
resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==}
|
||||
|
||||
'@vitest/spy@3.2.3':
|
||||
resolution: {integrity: sha512-JHu9Wl+7bf6FEejTCREy+DmgWe+rQKbK+y32C/k5f4TBIAlijhJbRBIRIOCEpVevgRsCQR2iHRUH2/qKVM/plw==}
|
||||
|
||||
'@vitest/spy@3.2.4':
|
||||
resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==}
|
||||
|
||||
'@vitest/utils@3.2.3':
|
||||
resolution: {integrity: sha512-4zFBCU5Pf+4Z6v+rwnZ1HU1yzOKKvDkMXZrymE2PBlbjKJRlrOxbvpfPSvJTGRIwGoahaOGvp+kbCoxifhzJ1Q==}
|
||||
|
||||
'@vitest/utils@3.2.4':
|
||||
resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==}
|
||||
|
||||
'@webassemblyjs/ast@1.14.1':
|
||||
resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==}
|
||||
|
||||
@ -4876,6 +4961,9 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
|
||||
jsonc-parser@3.3.1:
|
||||
resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==}
|
||||
|
||||
jsonfile@2.4.0:
|
||||
resolution: {integrity: sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw==}
|
||||
|
||||
@ -5002,6 +5090,9 @@ packages:
|
||||
loupe@3.1.3:
|
||||
resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==}
|
||||
|
||||
loupe@3.1.4:
|
||||
resolution: {integrity: sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==}
|
||||
|
||||
lower-case-first@2.0.2:
|
||||
resolution: {integrity: sha512-EVm/rR94FJTZi3zefZ82fLWab+GX14LJN4HrWBcuo6Evmsl9hEfnqxgcHCKb9q+mNf6EVdsjx/qucYFIIB84pg==}
|
||||
|
||||
@ -5952,6 +6043,9 @@ packages:
|
||||
simple-swizzle@0.2.2:
|
||||
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
|
||||
|
||||
sisteransi@1.0.5:
|
||||
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
|
||||
|
||||
slash@3.0.0:
|
||||
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
||||
engines: {node: '>=8'}
|
||||
@ -6260,6 +6354,10 @@ packages:
|
||||
resolution: {integrity: sha512-7CotroY9a8DKsKprEy/a14aCCm8jYVmR7aFy4fpkZM8sdpNJbKkixuNjgM50yCmip2ezc8z4N7k3oe2+rfRJCQ==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
|
||||
tinypool@1.1.1:
|
||||
resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
|
||||
tinyrainbow@2.0.0:
|
||||
resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@ -6384,8 +6482,8 @@ packages:
|
||||
uc.micro@2.1.0:
|
||||
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
|
||||
|
||||
ultracite@4.2.13:
|
||||
resolution: {integrity: sha512-j49R1z3xXIPhdvU19x0z0Z4hNewJYn4F1h42ULeaCOylBuxwGVE401piPxe3aVapwue7+Ec3J6wnL/+mW4zwww==}
|
||||
ultracite@5.0.32:
|
||||
resolution: {integrity: sha512-JjVNswL1mkIaOkPVh1nuEGnbEaCa94+ftqJ9hpRX2Y+jt72pcv32JeWg3Dqhkz/e3l449f7KAzMHO4x6IbxFZQ==}
|
||||
hasBin: true
|
||||
|
||||
unbox-primitive@1.1.0:
|
||||
@ -6492,6 +6590,11 @@ packages:
|
||||
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||
hasBin: true
|
||||
|
||||
vite-node@3.2.4:
|
||||
resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==}
|
||||
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||
hasBin: true
|
||||
|
||||
vite@5.4.11:
|
||||
resolution: {integrity: sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
@ -6551,6 +6654,34 @@ packages:
|
||||
jsdom:
|
||||
optional: true
|
||||
|
||||
vitest@3.2.4:
|
||||
resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==}
|
||||
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@edge-runtime/vm': '*'
|
||||
'@types/debug': ^4.1.12
|
||||
'@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
|
||||
'@vitest/browser': 3.2.4
|
||||
'@vitest/ui': 3.2.4
|
||||
happy-dom: '*'
|
||||
jsdom: '*'
|
||||
peerDependenciesMeta:
|
||||
'@edge-runtime/vm':
|
||||
optional: true
|
||||
'@types/debug':
|
||||
optional: true
|
||||
'@types/node':
|
||||
optional: true
|
||||
'@vitest/browser':
|
||||
optional: true
|
||||
'@vitest/ui':
|
||||
optional: true
|
||||
happy-dom:
|
||||
optional: true
|
||||
jsdom:
|
||||
optional: true
|
||||
|
||||
vscode-languageserver-types@3.17.5:
|
||||
resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==}
|
||||
|
||||
@ -7133,41 +7264,52 @@ snapshots:
|
||||
'@babel/helper-string-parser': 7.27.1
|
||||
'@babel/helper-validator-identifier': 7.27.1
|
||||
|
||||
'@biomejs/biome@1.9.4':
|
||||
'@biomejs/biome@2.1.1':
|
||||
optionalDependencies:
|
||||
'@biomejs/cli-darwin-arm64': 1.9.4
|
||||
'@biomejs/cli-darwin-x64': 1.9.4
|
||||
'@biomejs/cli-linux-arm64': 1.9.4
|
||||
'@biomejs/cli-linux-arm64-musl': 1.9.4
|
||||
'@biomejs/cli-linux-x64': 1.9.4
|
||||
'@biomejs/cli-linux-x64-musl': 1.9.4
|
||||
'@biomejs/cli-win32-arm64': 1.9.4
|
||||
'@biomejs/cli-win32-x64': 1.9.4
|
||||
'@biomejs/cli-darwin-arm64': 2.1.1
|
||||
'@biomejs/cli-darwin-x64': 2.1.1
|
||||
'@biomejs/cli-linux-arm64': 2.1.1
|
||||
'@biomejs/cli-linux-arm64-musl': 2.1.1
|
||||
'@biomejs/cli-linux-x64': 2.1.1
|
||||
'@biomejs/cli-linux-x64-musl': 2.1.1
|
||||
'@biomejs/cli-win32-arm64': 2.1.1
|
||||
'@biomejs/cli-win32-x64': 2.1.1
|
||||
|
||||
'@biomejs/cli-darwin-arm64@1.9.4':
|
||||
'@biomejs/cli-darwin-arm64@2.1.1':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-darwin-x64@1.9.4':
|
||||
'@biomejs/cli-darwin-x64@2.1.1':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-linux-arm64-musl@1.9.4':
|
||||
'@biomejs/cli-linux-arm64-musl@2.1.1':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-linux-arm64@1.9.4':
|
||||
'@biomejs/cli-linux-arm64@2.1.1':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-linux-x64-musl@1.9.4':
|
||||
'@biomejs/cli-linux-x64-musl@2.1.1':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-linux-x64@1.9.4':
|
||||
'@biomejs/cli-linux-x64@2.1.1':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-win32-arm64@1.9.4':
|
||||
'@biomejs/cli-win32-arm64@2.1.1':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-win32-x64@1.9.4':
|
||||
'@biomejs/cli-win32-x64@2.1.1':
|
||||
optional: true
|
||||
|
||||
'@clack/core@0.5.0':
|
||||
dependencies:
|
||||
picocolors: 1.1.1
|
||||
sisteransi: 1.0.5
|
||||
|
||||
'@clack/prompts@0.11.0':
|
||||
dependencies:
|
||||
'@clack/core': 0.5.0
|
||||
picocolors: 1.1.1
|
||||
sisteransi: 1.0.5
|
||||
|
||||
'@codemirror/language@6.11.1':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.5.2
|
||||
@ -9878,6 +10020,37 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 24.0.10
|
||||
|
||||
'@typescript/native-preview-darwin-arm64@7.0.0-dev.20250712.1':
|
||||
optional: true
|
||||
|
||||
'@typescript/native-preview-darwin-x64@7.0.0-dev.20250712.1':
|
||||
optional: true
|
||||
|
||||
'@typescript/native-preview-linux-arm64@7.0.0-dev.20250712.1':
|
||||
optional: true
|
||||
|
||||
'@typescript/native-preview-linux-arm@7.0.0-dev.20250712.1':
|
||||
optional: true
|
||||
|
||||
'@typescript/native-preview-linux-x64@7.0.0-dev.20250712.1':
|
||||
optional: true
|
||||
|
||||
'@typescript/native-preview-win32-arm64@7.0.0-dev.20250712.1':
|
||||
optional: true
|
||||
|
||||
'@typescript/native-preview-win32-x64@7.0.0-dev.20250712.1':
|
||||
optional: true
|
||||
|
||||
'@typescript/native-preview@7.0.0-dev.20250712.1':
|
||||
optionalDependencies:
|
||||
'@typescript/native-preview-darwin-arm64': 7.0.0-dev.20250712.1
|
||||
'@typescript/native-preview-darwin-x64': 7.0.0-dev.20250712.1
|
||||
'@typescript/native-preview-linux-arm': 7.0.0-dev.20250712.1
|
||||
'@typescript/native-preview-linux-arm64': 7.0.0-dev.20250712.1
|
||||
'@typescript/native-preview-linux-x64': 7.0.0-dev.20250712.1
|
||||
'@typescript/native-preview-win32-arm64': 7.0.0-dev.20250712.1
|
||||
'@typescript/native-preview-win32-x64': 7.0.0-dev.20250712.1
|
||||
|
||||
'@vitejs/plugin-react@4.5.2(vite@5.4.11(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1))':
|
||||
dependencies:
|
||||
'@babel/core': 7.27.4
|
||||
@ -9898,6 +10071,14 @@ snapshots:
|
||||
chai: 5.2.0
|
||||
tinyrainbow: 2.0.0
|
||||
|
||||
'@vitest/expect@3.2.4':
|
||||
dependencies:
|
||||
'@types/chai': 5.2.2
|
||||
'@vitest/spy': 3.2.4
|
||||
'@vitest/utils': 3.2.4
|
||||
chai: 5.2.0
|
||||
tinyrainbow: 2.0.0
|
||||
|
||||
'@vitest/mocker@3.2.3(vite@5.4.11(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1))':
|
||||
dependencies:
|
||||
'@vitest/spy': 3.2.3
|
||||
@ -9906,32 +10087,66 @@ snapshots:
|
||||
optionalDependencies:
|
||||
vite: 5.4.11(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1)
|
||||
|
||||
'@vitest/mocker@3.2.4(vite@5.4.11(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1))':
|
||||
dependencies:
|
||||
'@vitest/spy': 3.2.4
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.17
|
||||
optionalDependencies:
|
||||
vite: 5.4.11(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1)
|
||||
|
||||
'@vitest/pretty-format@3.2.3':
|
||||
dependencies:
|
||||
tinyrainbow: 2.0.0
|
||||
|
||||
'@vitest/pretty-format@3.2.4':
|
||||
dependencies:
|
||||
tinyrainbow: 2.0.0
|
||||
|
||||
'@vitest/runner@3.2.3':
|
||||
dependencies:
|
||||
'@vitest/utils': 3.2.3
|
||||
pathe: 2.0.3
|
||||
strip-literal: 3.0.0
|
||||
|
||||
'@vitest/runner@3.2.4':
|
||||
dependencies:
|
||||
'@vitest/utils': 3.2.4
|
||||
pathe: 2.0.3
|
||||
strip-literal: 3.0.0
|
||||
|
||||
'@vitest/snapshot@3.2.3':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 3.2.3
|
||||
magic-string: 0.30.17
|
||||
pathe: 2.0.3
|
||||
|
||||
'@vitest/snapshot@3.2.4':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 3.2.4
|
||||
magic-string: 0.30.17
|
||||
pathe: 2.0.3
|
||||
|
||||
'@vitest/spy@3.2.3':
|
||||
dependencies:
|
||||
tinyspy: 4.0.3
|
||||
|
||||
'@vitest/spy@3.2.4':
|
||||
dependencies:
|
||||
tinyspy: 4.0.3
|
||||
|
||||
'@vitest/utils@3.2.3':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 3.2.3
|
||||
loupe: 3.1.3
|
||||
tinyrainbow: 2.0.0
|
||||
|
||||
'@vitest/utils@3.2.4':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 3.2.4
|
||||
loupe: 3.1.4
|
||||
tinyrainbow: 2.0.0
|
||||
|
||||
'@webassemblyjs/ast@1.14.1':
|
||||
dependencies:
|
||||
'@webassemblyjs/helper-numbers': 1.13.2
|
||||
@ -11790,6 +12005,8 @@ snapshots:
|
||||
|
||||
json5@2.2.3: {}
|
||||
|
||||
jsonc-parser@3.3.1: {}
|
||||
|
||||
jsonfile@2.4.0:
|
||||
optionalDependencies:
|
||||
graceful-fs: 4.2.11
|
||||
@ -11907,6 +12124,8 @@ snapshots:
|
||||
|
||||
loupe@3.1.3: {}
|
||||
|
||||
loupe@3.1.4: {}
|
||||
|
||||
lower-case-first@2.0.2:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
@ -12943,6 +13162,8 @@ snapshots:
|
||||
is-arrayish: 0.3.2
|
||||
optional: true
|
||||
|
||||
sisteransi@1.0.5: {}
|
||||
|
||||
slash@3.0.0: {}
|
||||
|
||||
slice-ansi@3.0.0:
|
||||
@ -13264,6 +13485,8 @@ snapshots:
|
||||
|
||||
tinypool@1.1.0: {}
|
||||
|
||||
tinypool@1.1.1: {}
|
||||
|
||||
tinyrainbow@2.0.0: {}
|
||||
|
||||
tinyspy@4.0.3: {}
|
||||
@ -13390,9 +13613,30 @@ snapshots:
|
||||
|
||||
uc.micro@2.1.0: {}
|
||||
|
||||
ultracite@4.2.13:
|
||||
ultracite@5.0.32(@types/node@24.0.10)(jsdom@25.0.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1):
|
||||
dependencies:
|
||||
'@clack/prompts': 0.11.0
|
||||
commander: 14.0.0
|
||||
deepmerge: 4.3.1
|
||||
jsonc-parser: 3.3.1
|
||||
vitest: 3.2.4(@types/node@24.0.10)(jsdom@25.0.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1)
|
||||
transitivePeerDependencies:
|
||||
- '@edge-runtime/vm'
|
||||
- '@types/debug'
|
||||
- '@types/node'
|
||||
- '@vitest/browser'
|
||||
- '@vitest/ui'
|
||||
- happy-dom
|
||||
- jsdom
|
||||
- less
|
||||
- lightningcss
|
||||
- msw
|
||||
- sass
|
||||
- sass-embedded
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
unbox-primitive@1.1.0:
|
||||
dependencies:
|
||||
@ -13527,6 +13771,24 @@ snapshots:
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
vite-node@3.2.4(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.4.1
|
||||
es-module-lexer: 1.7.0
|
||||
pathe: 2.0.3
|
||||
vite: 5.4.11(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- less
|
||||
- lightningcss
|
||||
- sass
|
||||
- sass-embedded
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
vite@5.4.11(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1):
|
||||
dependencies:
|
||||
esbuild: 0.21.5
|
||||
@ -13578,6 +13840,45 @@ snapshots:
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
vitest@3.2.4(@types/node@24.0.10)(jsdom@25.0.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1):
|
||||
dependencies:
|
||||
'@types/chai': 5.2.2
|
||||
'@vitest/expect': 3.2.4
|
||||
'@vitest/mocker': 3.2.4(vite@5.4.11(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1))
|
||||
'@vitest/pretty-format': 3.2.4
|
||||
'@vitest/runner': 3.2.4
|
||||
'@vitest/snapshot': 3.2.4
|
||||
'@vitest/spy': 3.2.4
|
||||
'@vitest/utils': 3.2.4
|
||||
chai: 5.2.0
|
||||
debug: 4.4.1
|
||||
expect-type: 1.2.1
|
||||
magic-string: 0.30.17
|
||||
pathe: 2.0.3
|
||||
picomatch: 4.0.2
|
||||
std-env: 3.9.0
|
||||
tinybench: 2.9.0
|
||||
tinyexec: 0.3.2
|
||||
tinyglobby: 0.2.14
|
||||
tinypool: 1.1.1
|
||||
tinyrainbow: 2.0.0
|
||||
vite: 5.4.11(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1)
|
||||
vite-node: 3.2.4(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 24.0.10
|
||||
jsdom: 25.0.1(bufferutil@4.0.9)(utf-8-validate@6.0.5)
|
||||
transitivePeerDependencies:
|
||||
- less
|
||||
- lightningcss
|
||||
- msw
|
||||
- sass
|
||||
- sass-embedded
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
vscode-languageserver-types@3.17.5: {}
|
||||
|
||||
w3c-keyname@2.2.8: {}
|
||||
|
Loading…
Reference in New Issue
Block a user