konobangu/apps/webui/src/presentation/routes/_app/subscriptions/-cron-creation.tsx
2025-07-08 00:54:34 +08:00

259 lines
7.0 KiB
TypeScript

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,
CardAction,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Cron } from '@/components/ui/cron';
import { CronMode } from '@/components/ui/cron/types';
import {
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Separator } from '@/components/ui/separator';
import { Spinner } from '@/components/ui/spinner';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { INSERT_CRON } from '@/domains/recorder/schema/cron';
import { useInject } from '@/infra/di/inject';
import {
type InsertCronMutation,
type InsertCronMutationVariables,
SubscriberTaskTypeEnum,
} from '@/infra/graphql/gql/graphql';
import { IntlService } from '@/infra/intl/intl.service';
const SUBSCRIPTION_TASK_CRON_PRESETS = [
{
label: 'Every hour',
value: '0 0 * * * *',
description: 'Runs at the top of every hour',
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: '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 CRON_TABS = [
{
tab: SubscriberTaskTypeEnum.SyncOneSubscriptionSources,
label: 'Sync sources',
description: 'Syncs subscription sources',
},
{
tab: SubscriberTaskTypeEnum.SyncOneSubscriptionFeedsIncremental,
label: 'Feeds incremental',
description: 'Syncs incremental subscription feeds',
},
{
tab: SubscriberTaskTypeEnum.SyncOneSubscriptionFeedsFull,
label: 'Feeds full',
description: 'Syncs all subscription feeds',
},
];
export type SubscriptionCronCreationViewCompletePayload = {
id: number;
};
export interface SubscriptionCronCreationViewProps {
subscriptionId: number;
onComplete: (payload: SubscriptionCronCreationViewCompletePayload) => void;
}
export interface SubscriptionCronFormProps {
tab: (typeof CRON_TABS)[number];
timezone: string;
onComplete: (payload: SubscriptionCronFormPayload) => void;
}
export interface SubscriptionCronFormPayload {
cronExpr: string;
}
export const SubscriptionCronForm = memo(
({ tab, timezone, onComplete }: SubscriptionCronFormProps) => {
const [cronExpr, setCronExpr] = useState<string>('');
return (
<Card className="overflow-y-auto">
<CardHeader>
<CardTitle>{tab.label}</CardTitle>
<CardDescription>{tab.description}</CardDescription>
<CardAction>
<Button variant="default" onClick={() => onComplete({ cronExpr })}>
<CalendarIcon className="size-4" />
Create
</Button>
</CardAction>
</CardHeader>
<CardContent>
<Separator />
<Cron
mode={CronMode.Both}
withCard={false}
showPresets={false}
presets={SUBSCRIPTION_TASK_CRON_PRESETS}
timezone={timezone}
onChange={setCronExpr}
value={cronExpr}
/>
</CardContent>
</Card>
);
}
);
export const SubscriptionCronCreationView = memo(
({ subscriptionId, onComplete }: SubscriptionCronCreationViewProps) => {
const intlService = useInject(IntlService);
const [insertCron, { loading: loadingInsert }] = useMutation<
InsertCronMutation,
InsertCronMutationVariables
>(INSERT_CRON, {
onCompleted: (data) => {
toast.success('Cron created');
onComplete(data.cronCreateOne);
},
onError: (error) => {
toast.error('Failed to sync subscription', {
description: error.message,
});
},
});
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,
},
},
},
});
}}
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>
);
}
);
export interface SubscriptionCronCreationDialogContentProps {
subscriptionId: number;
onCancel?: VoidFunction;
}
export const SubscriptionCronCreationDialogContent = memo(
({
subscriptionId,
onCancel,
}: SubscriptionCronCreationDialogContentProps) => {
const navigate = useNavigate();
const handleCreationComplete = useCallback(
(payload: SubscriptionCronCreationViewCompletePayload) => {
navigate({
to: '/tasks/cron/detail/$id',
params: {
id: `${payload.id}`,
},
});
},
[navigate]
);
return (
<DialogContent
onAbort={onCancel}
className="flex max-h-[80vh] flex-col xl:max-w-2xl"
>
<DialogHeader>
<DialogTitle>Create Cron</DialogTitle>
<DialogDescription>
Create a cron to execute the subscription.
</DialogDescription>
</DialogHeader>
<SubscriptionCronCreationView
subscriptionId={subscriptionId}
onComplete={handleCreationComplete}
/>
</DialogContent>
);
}
);