mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-28 03:00:29 -04:00
improvement: schedule, files
This commit is contained in:
@@ -8,6 +8,10 @@ import { workflowSchedule } from '@/db/schema'
|
||||
|
||||
const logger = createLogger('ScheduledAPI')
|
||||
|
||||
// Track recent requests to reduce redundant logging
|
||||
const recentRequests = new Map<string, number>();
|
||||
const LOGGING_THROTTLE_MS = 5000; // 5 seconds between logging for the same workflow
|
||||
|
||||
/**
|
||||
* Get schedule information for a workflow
|
||||
*/
|
||||
@@ -15,6 +19,12 @@ export async function GET(req: NextRequest) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
const url = new URL(req.url)
|
||||
const workflowId = url.searchParams.get('workflowId')
|
||||
const mode = url.searchParams.get('mode')
|
||||
|
||||
// Skip processing if mode is provided and not 'schedule'
|
||||
if (mode && mode !== 'schedule') {
|
||||
return NextResponse.json({ schedule: null })
|
||||
}
|
||||
|
||||
try {
|
||||
const session = await getSession()
|
||||
@@ -27,7 +37,15 @@ export async function GET(req: NextRequest) {
|
||||
return NextResponse.json({ error: 'Missing workflowId parameter' }, { status: 400 })
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Getting schedule for workflow ${workflowId}`)
|
||||
// Check if we should log this request (throttle logging for repeat requests)
|
||||
const now = Date.now();
|
||||
const lastLog = recentRequests.get(workflowId) || 0;
|
||||
const shouldLog = now - lastLog > LOGGING_THROTTLE_MS;
|
||||
|
||||
if (shouldLog) {
|
||||
logger.info(`[${requestId}] Getting schedule for workflow ${workflowId}`)
|
||||
recentRequests.set(workflowId, now);
|
||||
}
|
||||
|
||||
// Find the schedule for this workflow
|
||||
const schedule = await db
|
||||
@@ -36,11 +54,15 @@ export async function GET(req: NextRequest) {
|
||||
.where(eq(workflowSchedule.workflowId, workflowId))
|
||||
.limit(1)
|
||||
|
||||
// Set cache control headers to reduce repeated API calls
|
||||
const headers = new Headers();
|
||||
headers.set('Cache-Control', 'max-age=30'); // Cache for 30 seconds
|
||||
|
||||
if (schedule.length === 0) {
|
||||
return NextResponse.json({ schedule: null })
|
||||
return NextResponse.json({ schedule: null }, { headers })
|
||||
}
|
||||
|
||||
return NextResponse.json({ schedule: schedule[0] })
|
||||
return NextResponse.json({ schedule: schedule[0] }, { headers })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error retrieving workflow schedule`, error)
|
||||
return NextResponse.json({ error: 'Failed to retrieve workflow schedule' }, { status: 500 })
|
||||
|
||||
@@ -54,17 +54,54 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
|
||||
const startWorkflow = getSubBlockValue(starterBlock, 'startWorkflow')
|
||||
const scheduleType = getSubBlockValue(starterBlock, 'scheduleType')
|
||||
|
||||
// If the workflow is not scheduled, delete any existing schedule
|
||||
if (startWorkflow !== 'schedule') {
|
||||
logger.info(`[${requestId}] Removing schedule for workflow ${workflowId}`)
|
||||
// Check if there's a valid schedule configuration
|
||||
const hasScheduleConfig = (() => {
|
||||
const getValue = (id: string): string => {
|
||||
const value = getSubBlockValue(starterBlock, id);
|
||||
return value && value.trim() !== '' ? value : '';
|
||||
};
|
||||
|
||||
if (scheduleType === 'minutes' && getValue('minutesInterval')) {
|
||||
return true;
|
||||
}
|
||||
if (scheduleType === 'hourly' && getValue('hourlyMinute') !== '') {
|
||||
return true;
|
||||
}
|
||||
if (scheduleType === 'daily' && getValue('dailyTime')) {
|
||||
return true;
|
||||
}
|
||||
if (scheduleType === 'weekly' && getValue('weeklyDay') &&
|
||||
getValue('weeklyDayTime')) {
|
||||
return true;
|
||||
}
|
||||
if (scheduleType === 'monthly' && getValue('monthlyDay') &&
|
||||
getValue('monthlyTime')) {
|
||||
return true;
|
||||
}
|
||||
if (scheduleType === 'custom' && getValue('cronExpression')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})();
|
||||
|
||||
// If the workflow is not configured for scheduling, delete any existing schedule
|
||||
if (startWorkflow !== 'schedule' && !hasScheduleConfig) {
|
||||
logger.info(`[${requestId}] Removing schedule for workflow ${workflowId} - no valid configuration found`)
|
||||
await db.delete(workflowSchedule).where(eq(workflowSchedule.workflowId, workflowId))
|
||||
|
||||
return NextResponse.json({ message: 'Schedule removed' })
|
||||
}
|
||||
|
||||
// If we're here, we either have startWorkflow === 'schedule' or hasScheduleConfig is true
|
||||
if (startWorkflow !== 'schedule') {
|
||||
logger.info(`[${requestId}] Setting workflow to scheduled mode based on schedule configuration`)
|
||||
// The UI should handle this, but as a fallback we'll assume the user intended to schedule
|
||||
// the workflow even if startWorkflow wasn't set properly
|
||||
}
|
||||
|
||||
// Get schedule configuration from starter block
|
||||
const scheduleType = getSubBlockValue(starterBlock, 'scheduleType')
|
||||
logger.debug(`[${requestId}] Schedule type for workflow ${workflowId}: ${scheduleType}`)
|
||||
|
||||
// Calculate cron expression based on schedule type
|
||||
|
||||
@@ -451,6 +451,11 @@ export function MarketplaceModal({ open, onOpenChange }: MarketplaceModalProps)
|
||||
disabled={isUnpublishing}
|
||||
className="gap-2"
|
||||
>
|
||||
{isUnpublishing ? (
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent mr-2" />
|
||||
) : (
|
||||
<Trash className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
{isUnpublishing ? 'Unpublishing...' : 'Unpublish'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { Calendar } from 'lucide-react'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { formatDateTime } from '@/lib/utils'
|
||||
|
||||
const logger = createLogger('ScheduleStatus')
|
||||
|
||||
interface ScheduleStatusProps {
|
||||
blockId: string
|
||||
}
|
||||
|
||||
export function ScheduleStatus({ blockId }: ScheduleStatusProps) {
|
||||
const [scheduleId, setScheduleId] = useState<string | null>(null)
|
||||
const [nextRunAt, setNextRunAt] = useState<string | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const params = useParams()
|
||||
const workflowId = params.id as string
|
||||
|
||||
// Check if schedule exists in the database
|
||||
useEffect(() => {
|
||||
const checkSchedule = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
// Check if there's a schedule for this workflow
|
||||
const response = await fetch(`/api/schedules?workflowId=${workflowId}`)
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
if (data.schedule) {
|
||||
setScheduleId(data.schedule.id)
|
||||
setNextRunAt(data.schedule.nextRunAt)
|
||||
} else {
|
||||
setScheduleId(null)
|
||||
setNextRunAt(null)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error checking schedule:', { error })
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
checkSchedule()
|
||||
}, [workflowId])
|
||||
|
||||
if (isLoading || !scheduleId || !nextRunAt) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="ml-auto bg-green-100 text-green-700 border-green-200
|
||||
dark:bg-green-900 dark:text-green-300 dark:border-green-800
|
||||
px-2 py-0.5 flex items-center"
|
||||
>
|
||||
<Calendar className="h-3.5 w-3.5 mr-1" />
|
||||
<span className="text-xs">Active</span>
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<div className="text-xs">
|
||||
<div>
|
||||
<strong>Schedule Active</strong>
|
||||
</div>
|
||||
<div>Next run: {formatDateTime(new Date(nextRunAt))}</div>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
@@ -309,7 +309,6 @@ export function FileUpload({
|
||||
useSubBlockStore.getState().setValue(blockId, subBlockId, null)
|
||||
}
|
||||
|
||||
addNotification('console', `${file.name} was deleted successfully`, activeWorkflowId)
|
||||
useWorkflowStore.getState().triggerUpdate()
|
||||
} catch (error) {
|
||||
addNotification(
|
||||
@@ -387,19 +386,6 @@ export function FileUpload({
|
||||
}
|
||||
}
|
||||
|
||||
// Show a single consolidated notification about the deletions
|
||||
if (deletionResults.success > 0) {
|
||||
if (fileCount === 1) {
|
||||
addNotification('console', `File was deleted successfully`, activeWorkflowId)
|
||||
} else {
|
||||
addNotification(
|
||||
'console',
|
||||
`${deletionResults.success} of ${fileCount} files were deleted successfully`,
|
||||
activeWorkflowId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Show error notification if any deletions failed
|
||||
if (deletionResults.failures.length > 0) {
|
||||
if (deletionResults.failures.length === 1) {
|
||||
@@ -427,10 +413,10 @@ export function FileUpload({
|
||||
return (
|
||||
<div
|
||||
key={file.path}
|
||||
className="flex items-center justify-between p-2 rounded border border-border bg-secondary/30"
|
||||
className="flex items-center justify-between px-3 py-2 rounded border border-border bg-background"
|
||||
>
|
||||
<div className="flex-1 truncate pr-2">
|
||||
<div className="font-medium text-sm truncate">{file.name}</div>
|
||||
<div className="font-normal text-sm truncate">{file.name}</div>
|
||||
<div className="text-xs text-muted-foreground">{formatFileSize(file.size)}</div>
|
||||
</div>
|
||||
<Button
|
||||
@@ -442,7 +428,7 @@ export function FileUpload({
|
||||
disabled={isDeleting}
|
||||
>
|
||||
{isDeleting ? (
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent" />
|
||||
) : (
|
||||
<X className="h-4 w-4" />
|
||||
)}
|
||||
@@ -456,14 +442,14 @@ export function FileUpload({
|
||||
return (
|
||||
<div
|
||||
key={file.id}
|
||||
className="flex items-center justify-between p-2 rounded border border-border bg-secondary/30"
|
||||
className="flex items-center justify-between px-3 py-2 rounded border border-border bg-background"
|
||||
>
|
||||
<div className="flex-1 truncate pr-2">
|
||||
<div className="font-medium text-sm truncate">{file.name}</div>
|
||||
<div className="font-normal text-sm truncate">{file.name}</div>
|
||||
<div className="text-xs text-muted-foreground">{formatFileSize(file.size)}</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-center h-8 w-8 shrink-0">
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -486,10 +472,10 @@ export function FileUpload({
|
||||
data-testid="file-input-element"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div className="bg-background">
|
||||
{/* File list with consistent spacing */}
|
||||
{(hasFiles || isUploading) && (
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-2 mb-2">
|
||||
{/* Only show files that aren't currently uploading */}
|
||||
{filesArray.map((file) => {
|
||||
// Don't show files that have duplicates in the uploading list
|
||||
@@ -514,12 +500,12 @@ export function FileUpload({
|
||||
|
||||
{/* Action buttons */}
|
||||
{(hasFiles || isUploading) && (
|
||||
<div className="flex space-x-2 mt-2">
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
className="flex-1 h-10 text-sm font-normal"
|
||||
onClick={handleRemoveAllFiles}
|
||||
disabled={isUploading}
|
||||
>
|
||||
@@ -530,7 +516,7 @@ export function FileUpload({
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
className="flex-1 h-10 text-sm font-normal"
|
||||
onClick={handleOpenFileDialog}
|
||||
>
|
||||
Add More
|
||||
@@ -546,22 +532,15 @@ export function FileUpload({
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full justify-center text-center font-normal"
|
||||
className="w-full justify-center text-center h-10 bg-background text-sm font-normal"
|
||||
onClick={handleOpenFileDialog}
|
||||
>
|
||||
<Upload className="mr-2 h-4 w-4" />
|
||||
{multiple ? 'Upload Files' : 'Upload File'}
|
||||
<div className="flex items-center justify-center gap-2 w-full">
|
||||
{/* <Upload className="h-4 w-4" /> */}
|
||||
<span>{multiple ? 'Upload Files' : 'Upload File'}</span>
|
||||
<span className="text-xs text-muted-foreground">({maxSize}MB max)</span>
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger className="ml-2">
|
||||
<Info className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Max file size: {maxSize}MB</p>
|
||||
{multiple && <p>You can select multiple files at once</p>}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { format } from 'date-fns'
|
||||
import { Calendar, Trash2 } from 'lucide-react'
|
||||
import { Trash2, X } from 'lucide-react'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -14,13 +14,7 @@ import {
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Calendar as CalendarComponent } from '@/components/ui/calendar'
|
||||
import {
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import {
|
||||
@@ -31,6 +25,7 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { UnsavedChangesDialog } from '../../../components/webhook/components/ui/confirmation'
|
||||
import { useSubBlockValue } from '../../../hooks/use-sub-block-value'
|
||||
import { TimeInput } from '../../time-input'
|
||||
@@ -70,6 +65,9 @@ export function ScheduleModal({
|
||||
const [cronExpression, setCronExpression] = useSubBlockValue(blockId, 'cronExpression')
|
||||
const [timezone, setTimezone] = useSubBlockValue(blockId, 'timezone')
|
||||
|
||||
// Get the startWorkflow value at the component level
|
||||
const [startWorkflow, setStartWorkflow] = useSubBlockValue(blockId, 'startWorkflow')
|
||||
|
||||
// UI states
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
@@ -254,6 +252,18 @@ export function ScheduleModal({
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure the block's startWorkflow field is set to 'schedule'
|
||||
// This is critical to ensure the schedule is actually enabled
|
||||
logger.debug('Current startWorkflow value:', startWorkflow)
|
||||
|
||||
if (startWorkflow !== 'schedule') {
|
||||
logger.debug('Setting startWorkflow to schedule')
|
||||
setStartWorkflow('schedule')
|
||||
|
||||
// Give the state time to update
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
}
|
||||
|
||||
const success = await onSave()
|
||||
|
||||
if (success) {
|
||||
@@ -272,6 +282,7 @@ export function ScheduleModal({
|
||||
timezone: timezone || 'UTC',
|
||||
cronExpression: cronExpression || '',
|
||||
}
|
||||
logger.debug('Schedule saved successfully, updating initial values', updatedValues)
|
||||
setInitialValues(updatedValues)
|
||||
setHasChanges(false)
|
||||
onClose()
|
||||
@@ -320,263 +331,291 @@ export function ScheduleModal({
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center">
|
||||
<Calendar className="h-5 w-5 mr-3 text-blue-500" />
|
||||
<div>
|
||||
<DialogTitle>Schedule Configuration</DialogTitle>
|
||||
<DialogDescription>
|
||||
Configure when your workflow should run automatically
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<DialogContent className="sm:max-w-[600px] flex flex-col p-0 gap-0" hideCloseButton>
|
||||
<DialogHeader className="px-6 py-4 border-b">
|
||||
<div className="flex items-center justify-between">
|
||||
<DialogTitle className="text-lg font-medium">Schedule Configuration</DialogTitle>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 p-0" onClick={handleClose}>
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</Button>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
{errorMessage && (
|
||||
<Alert variant="destructive" className="my-2">
|
||||
<AlertDescription>{errorMessage}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="grid gap-4 py-4">
|
||||
{/* Common date and time fields */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<label htmlFor="scheduleStartAt" className="text-sm font-medium">
|
||||
Start At
|
||||
</label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
id="scheduleStartAt"
|
||||
variant="outline"
|
||||
className="justify-start text-left font-normal"
|
||||
>
|
||||
<Calendar className="mr-2 h-4 w-4" />
|
||||
{formatDate(scheduleStartAt || '')}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<CalendarComponent
|
||||
mode="single"
|
||||
selected={scheduleStartAt ? new Date(scheduleStartAt) : undefined}
|
||||
onSelect={(date) => setScheduleStartAt(date ? date.toISOString() : '')}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<label htmlFor="scheduleTime" className="text-sm font-medium">
|
||||
Time
|
||||
</label>
|
||||
<TimeInput blockId={blockId} subBlockId="scheduleTime" placeholder="Select time" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Frequency selector */}
|
||||
<div className="grid gap-2">
|
||||
<label htmlFor="scheduleType" className="text-sm font-medium">
|
||||
Frequency
|
||||
</label>
|
||||
<Select
|
||||
value={scheduleType || 'daily'}
|
||||
onValueChange={(value) => setScheduleType(value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select frequency" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="minutes">Every X Minutes</SelectItem>
|
||||
<SelectItem value="hourly">Hourly</SelectItem>
|
||||
<SelectItem value="daily">Daily</SelectItem>
|
||||
<SelectItem value="weekly">Weekly</SelectItem>
|
||||
<SelectItem value="monthly">Monthly</SelectItem>
|
||||
<SelectItem value="custom">Custom Cron</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Minutes schedule options */}
|
||||
{scheduleType === 'minutes' && (
|
||||
<div className="grid gap-2">
|
||||
<label htmlFor="minutesInterval" className="text-sm font-medium">
|
||||
Run Every (minutes)
|
||||
</label>
|
||||
<Input
|
||||
id="minutesInterval"
|
||||
value={minutesInterval || ''}
|
||||
onChange={(e) => setMinutesInterval(e.target.value)}
|
||||
placeholder="15"
|
||||
type="number"
|
||||
min="1"
|
||||
/>
|
||||
</div>
|
||||
<div className="pt-4 px-6 pb-6 overflow-y-auto">
|
||||
{errorMessage && (
|
||||
<Alert variant="destructive" className="mb-4">
|
||||
<AlertDescription>{errorMessage}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Hourly schedule options */}
|
||||
{scheduleType === 'hourly' && (
|
||||
<div className="grid gap-2">
|
||||
<label htmlFor="hourlyMinute" className="text-sm font-medium">
|
||||
Minute of the Hour
|
||||
</label>
|
||||
<Input
|
||||
id="hourlyMinute"
|
||||
value={hourlyMinute || ''}
|
||||
onChange={(e) => setHourlyMinute(e.target.value)}
|
||||
placeholder="0"
|
||||
type="number"
|
||||
min="0"
|
||||
max="59"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Specify which minute of each hour the workflow should run (0-59)
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Daily schedule options */}
|
||||
{scheduleType === 'daily' && (
|
||||
<div className="grid gap-2">
|
||||
<label htmlFor="dailyTime" className="text-sm font-medium">
|
||||
Time of Day
|
||||
</label>
|
||||
<TimeInput blockId={blockId} subBlockId="dailyTime" placeholder="Select time" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Weekly schedule options */}
|
||||
{scheduleType === 'weekly' && (
|
||||
<>
|
||||
<div className="grid gap-2">
|
||||
<label htmlFor="weeklyDay" className="text-sm font-medium">
|
||||
Day of Week
|
||||
<div className="space-y-6">
|
||||
{/* Common date and time fields */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1">
|
||||
<label htmlFor="scheduleStartAt" className="text-sm font-medium">
|
||||
Start At
|
||||
</label>
|
||||
<Select value={weeklyDay || 'MON'} onValueChange={(value) => setWeeklyDay(value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select day" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="MON">Monday</SelectItem>
|
||||
<SelectItem value="TUE">Tuesday</SelectItem>
|
||||
<SelectItem value="WED">Wednesday</SelectItem>
|
||||
<SelectItem value="THU">Thursday</SelectItem>
|
||||
<SelectItem value="FRI">Friday</SelectItem>
|
||||
<SelectItem value="SAT">Saturday</SelectItem>
|
||||
<SelectItem value="SUN">Sunday</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
id="scheduleStartAt"
|
||||
variant="outline"
|
||||
className="justify-start text-left font-normal w-full h-10"
|
||||
>
|
||||
{formatDate(scheduleStartAt || '')}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<CalendarComponent
|
||||
mode="single"
|
||||
selected={scheduleStartAt ? new Date(scheduleStartAt) : undefined}
|
||||
onSelect={(date) => setScheduleStartAt(date ? date.toISOString() : '')}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<label htmlFor="weeklyDayTime" className="text-sm font-medium">
|
||||
Time of Day
|
||||
<div className="space-y-1">
|
||||
<label htmlFor="scheduleTime" className="text-sm font-medium">
|
||||
Time
|
||||
</label>
|
||||
<TimeInput blockId={blockId} subBlockId="weeklyDayTime" placeholder="Select time" />
|
||||
<TimeInput
|
||||
blockId={blockId}
|
||||
subBlockId="scheduleTime"
|
||||
placeholder="Select time"
|
||||
className="h-10"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Monthly schedule options */}
|
||||
{scheduleType === 'monthly' && (
|
||||
<>
|
||||
<div className="grid gap-2">
|
||||
<label htmlFor="monthlyDay" className="text-sm font-medium">
|
||||
Day of Month
|
||||
{/* Frequency selector */}
|
||||
<div className="space-y-1">
|
||||
<label htmlFor="scheduleType" className="text-sm font-medium">
|
||||
Frequency
|
||||
</label>
|
||||
<Select
|
||||
value={scheduleType || 'daily'}
|
||||
onValueChange={(value) => setScheduleType(value)}
|
||||
>
|
||||
<SelectTrigger className="h-10">
|
||||
<SelectValue placeholder="Select frequency" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="minutes">Every X Minutes</SelectItem>
|
||||
<SelectItem value="hourly">Hourly</SelectItem>
|
||||
<SelectItem value="daily">Daily</SelectItem>
|
||||
<SelectItem value="weekly">Weekly</SelectItem>
|
||||
<SelectItem value="monthly">Monthly</SelectItem>
|
||||
<SelectItem value="custom">Custom Cron</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Minutes schedule options */}
|
||||
{scheduleType === 'minutes' && (
|
||||
<div className="space-y-1">
|
||||
<label htmlFor="minutesInterval" className="text-sm font-medium">
|
||||
Run Every (minutes)
|
||||
</label>
|
||||
<Input
|
||||
id="monthlyDay"
|
||||
value={monthlyDay || ''}
|
||||
onChange={(e) => setMonthlyDay(e.target.value)}
|
||||
placeholder="1"
|
||||
id="minutesInterval"
|
||||
value={minutesInterval || ''}
|
||||
onChange={(e) => setMinutesInterval(e.target.value)}
|
||||
placeholder="15"
|
||||
type="number"
|
||||
min="1"
|
||||
max="31"
|
||||
className="h-10"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Hourly schedule options */}
|
||||
{scheduleType === 'hourly' && (
|
||||
<div className="space-y-1">
|
||||
<label htmlFor="hourlyMinute" className="text-sm font-medium">
|
||||
Minute of the Hour
|
||||
</label>
|
||||
<Input
|
||||
id="hourlyMinute"
|
||||
value={hourlyMinute || ''}
|
||||
onChange={(e) => setHourlyMinute(e.target.value)}
|
||||
placeholder="0"
|
||||
type="number"
|
||||
min="0"
|
||||
max="59"
|
||||
className="h-10"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Specify which day of the month the workflow should run (1-31)
|
||||
Specify which minute of each hour the workflow should run (0-59)
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid gap-2">
|
||||
<label htmlFor="monthlyTime" className="text-sm font-medium">
|
||||
{/* Daily schedule options */}
|
||||
{scheduleType === 'daily' && (
|
||||
<div className="space-y-1">
|
||||
<label htmlFor="dailyTime" className="text-sm font-medium">
|
||||
Time of Day
|
||||
</label>
|
||||
<TimeInput blockId={blockId} subBlockId="monthlyTime" placeholder="Select time" />
|
||||
<TimeInput
|
||||
blockId={blockId}
|
||||
subBlockId="dailyTime"
|
||||
placeholder="Select time"
|
||||
className="h-10"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* Custom cron options */}
|
||||
{scheduleType === 'custom' && (
|
||||
<div className="grid gap-2">
|
||||
<label htmlFor="cronExpression" className="text-sm font-medium">
|
||||
Cron Expression
|
||||
{/* Weekly schedule options */}
|
||||
{scheduleType === 'weekly' && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<label htmlFor="weeklyDay" className="text-sm font-medium">
|
||||
Day of Week
|
||||
</label>
|
||||
<Select value={weeklyDay || 'MON'} onValueChange={(value) => setWeeklyDay(value)}>
|
||||
<SelectTrigger className="h-10">
|
||||
<SelectValue placeholder="Select day" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="MON">Monday</SelectItem>
|
||||
<SelectItem value="TUE">Tuesday</SelectItem>
|
||||
<SelectItem value="WED">Wednesday</SelectItem>
|
||||
<SelectItem value="THU">Thursday</SelectItem>
|
||||
<SelectItem value="FRI">Friday</SelectItem>
|
||||
<SelectItem value="SAT">Saturday</SelectItem>
|
||||
<SelectItem value="SUN">Sunday</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label htmlFor="weeklyDayTime" className="text-sm font-medium">
|
||||
Time of Day
|
||||
</label>
|
||||
<TimeInput
|
||||
blockId={blockId}
|
||||
subBlockId="weeklyDayTime"
|
||||
placeholder="Select time"
|
||||
className="h-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Monthly schedule options */}
|
||||
{scheduleType === 'monthly' && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<label htmlFor="monthlyDay" className="text-sm font-medium">
|
||||
Day of Month
|
||||
</label>
|
||||
<Input
|
||||
id="monthlyDay"
|
||||
value={monthlyDay || ''}
|
||||
onChange={(e) => setMonthlyDay(e.target.value)}
|
||||
placeholder="1"
|
||||
type="number"
|
||||
min="1"
|
||||
max="31"
|
||||
className="h-10"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Specify which day of the month the workflow should run (1-31)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label htmlFor="monthlyTime" className="text-sm font-medium">
|
||||
Time of Day
|
||||
</label>
|
||||
<TimeInput
|
||||
blockId={blockId}
|
||||
subBlockId="monthlyTime"
|
||||
placeholder="Select time"
|
||||
className="h-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Custom cron options */}
|
||||
{scheduleType === 'custom' && (
|
||||
<div className="space-y-1">
|
||||
<label htmlFor="cronExpression" className="text-sm font-medium">
|
||||
Cron Expression
|
||||
</label>
|
||||
<Input
|
||||
id="cronExpression"
|
||||
value={cronExpression || ''}
|
||||
onChange={(e) => setCronExpression(e.target.value)}
|
||||
placeholder="*/15 * * * *"
|
||||
className="h-10"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Use standard cron format (e.g., "*/15 * * * *" for every 15 minutes)
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Timezone configuration */}
|
||||
<div className="space-y-1">
|
||||
<label htmlFor="timezone" className="text-sm font-medium">
|
||||
Timezone
|
||||
</label>
|
||||
<Input
|
||||
id="cronExpression"
|
||||
value={cronExpression || ''}
|
||||
onChange={(e) => setCronExpression(e.target.value)}
|
||||
placeholder="*/15 * * * *"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Use standard cron format (e.g., "*/15 * * * *" for every 15 minutes)
|
||||
</p>
|
||||
<Select value={timezone || 'UTC'} onValueChange={(value) => setTimezone(value)}>
|
||||
<SelectTrigger className="h-10">
|
||||
<SelectValue placeholder="Select timezone" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="UTC">UTC</SelectItem>
|
||||
<SelectItem value="America/New_York">US Eastern (UTC-4)</SelectItem>
|
||||
<SelectItem value="America/Chicago">US Central (UTC-5)</SelectItem>
|
||||
<SelectItem value="America/Denver">US Mountain (UTC-6)</SelectItem>
|
||||
<SelectItem value="America/Los_Angeles">US Pacific (UTC-7)</SelectItem>
|
||||
<SelectItem value="Europe/London">London (UTC+1)</SelectItem>
|
||||
<SelectItem value="Europe/Paris">Paris (UTC+2)</SelectItem>
|
||||
<SelectItem value="Asia/Singapore">Singapore (UTC+8)</SelectItem>
|
||||
<SelectItem value="Asia/Tokyo">Tokyo (UTC+9)</SelectItem>
|
||||
<SelectItem value="Australia/Sydney">Sydney (UTC+10)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Timezone configuration */}
|
||||
<div className="grid gap-2">
|
||||
<label htmlFor="timezone" className="text-sm font-medium">
|
||||
Timezone
|
||||
</label>
|
||||
<Select value={timezone || 'UTC'} onValueChange={(value) => setTimezone(value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select timezone" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="UTC">UTC</SelectItem>
|
||||
<SelectItem value="America/New_York">US Eastern (UTC-4)</SelectItem>
|
||||
<SelectItem value="America/Chicago">US Central (UTC-5)</SelectItem>
|
||||
<SelectItem value="America/Denver">US Mountain (UTC-6)</SelectItem>
|
||||
<SelectItem value="America/Los_Angeles">US Pacific (UTC-7)</SelectItem>
|
||||
<SelectItem value="Europe/London">London (UTC+1)</SelectItem>
|
||||
<SelectItem value="Europe/Paris">Paris (UTC+2)</SelectItem>
|
||||
<SelectItem value="Asia/Singapore">Singapore (UTC+8)</SelectItem>
|
||||
<SelectItem value="Asia/Tokyo">Tokyo (UTC+9)</SelectItem>
|
||||
<SelectItem value="Australia/Sydney">Sydney (UTC+10)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex justify-between">
|
||||
<div>
|
||||
{scheduleId && onDelete && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
onClick={openDeleteConfirm}
|
||||
disabled={isDeleting || isSaving}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
{isDeleting ? 'Deleting...' : 'Delete Schedule'}
|
||||
<DialogFooter className="px-6 pt-0 pb-6 w-full">
|
||||
<div className="flex w-full justify-between">
|
||||
<div>
|
||||
{scheduleId && onDelete && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
onClick={openDeleteConfirm}
|
||||
disabled={isDeleting || isSaving}
|
||||
size="default"
|
||||
className="h-10"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
{isDeleting ? 'Deleting...' : 'Delete Schedule'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={handleClose} size="default" className="h-10">
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={!hasChanges || isSaving}
|
||||
className={hasChanges ? 'bg-primary hover:bg-primary/90' : ''}
|
||||
>
|
||||
{isSaving ? 'Saving...' : 'Save Changes'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={!hasChanges || isSaving}
|
||||
className={cn('h-10', hasChanges ? 'bg-primary hover:bg-primary/90' : '')}
|
||||
size="default"
|
||||
>
|
||||
{isSaving ? 'Saving...' : 'Save Changes'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { Calendar, ExternalLink } from 'lucide-react'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog } from '@/components/ui/dialog'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
@@ -24,10 +23,12 @@ export function ScheduleConfig({ blockId, subBlockId, isConnecting }: ScheduleCo
|
||||
const [nextRunAt, setNextRunAt] = useState<string | null>(null)
|
||||
const [lastRanAt, setLastRanAt] = useState<string | null>(null)
|
||||
const [cronExpression, setCronExpression] = useState<string | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
// Track when we need to force a refresh of schedule data
|
||||
const [refreshCounter, setRefreshCounter] = useState(0)
|
||||
|
||||
const params = useParams()
|
||||
const workflowId = params.id as string
|
||||
@@ -36,41 +37,66 @@ export function ScheduleConfig({ blockId, subBlockId, isConnecting }: ScheduleCo
|
||||
const blocks = useWorkflowStore((state) => state.blocks)
|
||||
const edges = useWorkflowStore((state) => state.edges)
|
||||
const loops = useWorkflowStore((state) => state.loops)
|
||||
const triggerUpdate = useWorkflowStore((state) => state.triggerUpdate)
|
||||
const setScheduleStatus = useWorkflowStore((state) => state.setScheduleStatus)
|
||||
|
||||
// Get the schedule type from the block state
|
||||
const [scheduleType] = useSubBlockValue(blockId, 'scheduleType')
|
||||
|
||||
// Check if schedule exists in the database
|
||||
useEffect(() => {
|
||||
const checkSchedule = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
// Check if there's a schedule for this workflow
|
||||
const response = await fetch(`/api/schedules?workflowId=${workflowId}`)
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
if (data.schedule) {
|
||||
setScheduleId(data.schedule.id)
|
||||
setNextRunAt(data.schedule.nextRunAt)
|
||||
setLastRanAt(data.schedule.lastRanAt)
|
||||
setCronExpression(data.schedule.cronExpression)
|
||||
} else {
|
||||
setScheduleId(null)
|
||||
setNextRunAt(null)
|
||||
setLastRanAt(null)
|
||||
setCronExpression(null)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error checking schedule:', { error })
|
||||
setError('Failed to check schedule status')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
// Get the startWorkflow value to determine if scheduling is enabled
|
||||
// and expose the setter so we can update it
|
||||
const [startWorkflow, setStartWorkflow] = useSubBlockValue(blockId, 'startWorkflow')
|
||||
const isScheduleEnabled = startWorkflow === 'schedule'
|
||||
|
||||
// Function to check if schedule exists in the database
|
||||
const checkSchedule = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
// Check if there's a schedule for this workflow, passing the mode parameter
|
||||
const response = await fetch(`/api/schedules?workflowId=${workflowId}&mode=schedule`, {
|
||||
// Add cache: 'no-store' to prevent caching of this request
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
},
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
logger.debug(`Schedule check response:`, data)
|
||||
|
||||
if (data.schedule) {
|
||||
setScheduleId(data.schedule.id)
|
||||
setNextRunAt(data.schedule.nextRunAt)
|
||||
setLastRanAt(data.schedule.lastRanAt)
|
||||
setCronExpression(data.schedule.cronExpression)
|
||||
|
||||
// Set active schedule flag to true since we found an active schedule
|
||||
setScheduleStatus(true)
|
||||
} else {
|
||||
setScheduleId(null)
|
||||
setNextRunAt(null)
|
||||
setLastRanAt(null)
|
||||
setCronExpression(null)
|
||||
|
||||
// Set active schedule flag to false since no schedule was found
|
||||
setScheduleStatus(false)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error checking schedule:', { error })
|
||||
setError('Failed to check schedule status')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for schedule on mount and when relevant dependencies change
|
||||
useEffect(() => {
|
||||
// Always check for schedules regardless of the UI setting
|
||||
// This ensures we detect schedules even when the UI is set to manual
|
||||
checkSchedule()
|
||||
}, [workflowId, scheduleType, isModalOpen]) // Re-check when modal closes
|
||||
}, [workflowId, scheduleType, isModalOpen, refreshCounter])
|
||||
|
||||
// Format the schedule information for display
|
||||
const getScheduleInfo = () => {
|
||||
@@ -93,13 +119,13 @@ export function ScheduleConfig({ blockId, subBlockId, isConnecting }: ScheduleCo
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
<div className="flex items-center">
|
||||
<span className="font-medium">{scheduleTiming}</span>
|
||||
<>
|
||||
<div className="font-normal text-sm truncate">{scheduleTiming}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
<div>Next run: {formatDateTime(new Date(nextRunAt))}</div>
|
||||
{lastRanAt && <div>Last run: {formatDateTime(new Date(lastRanAt))}</div>}
|
||||
</div>
|
||||
<div>Next run: {formatDateTime(new Date(nextRunAt))}</div>
|
||||
{lastRanAt && <div>Last run: {formatDateTime(new Date(lastRanAt))}</div>}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -109,11 +135,24 @@ export function ScheduleConfig({ blockId, subBlockId, isConnecting }: ScheduleCo
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setIsModalOpen(false)
|
||||
// Force a refresh when closing the modal
|
||||
setRefreshCounter((prev) => prev + 1)
|
||||
}
|
||||
|
||||
const handleSaveSchedule = async (): Promise<boolean> => {
|
||||
setIsSaving(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
// Make sure that startWorkflow is set to 'schedule'
|
||||
if (startWorkflow !== 'schedule') {
|
||||
// Set startWorkflow to 'schedule' to enable scheduling
|
||||
setStartWorkflow('schedule')
|
||||
|
||||
// Give a moment for the state to update
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
}
|
||||
|
||||
// Send the complete workflow state to be saved/updated
|
||||
const response = await fetch(`/api/schedules/schedule`, {
|
||||
method: 'POST',
|
||||
@@ -130,12 +169,43 @@ export function ScheduleConfig({ blockId, subBlockId, isConnecting }: ScheduleCo
|
||||
}),
|
||||
})
|
||||
|
||||
// Clone the response to read it as text and parse as JSON
|
||||
const responseText = await response.text()
|
||||
let responseData
|
||||
try {
|
||||
responseData = JSON.parse(responseText)
|
||||
} catch (e) {
|
||||
logger.error('Failed to parse response JSON', e, responseText)
|
||||
responseData = {}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json()
|
||||
setError(data.error || 'Failed to save schedule')
|
||||
setError(responseData.error || 'Failed to save schedule')
|
||||
return false
|
||||
}
|
||||
|
||||
logger.debug('Schedule save response:', responseData)
|
||||
|
||||
// Update our local state with the response data
|
||||
// This allows showing schedule info immediately without waiting for another API call
|
||||
if (responseData.cronExpression) {
|
||||
setCronExpression(responseData.cronExpression)
|
||||
}
|
||||
|
||||
if (responseData.nextRunAt) {
|
||||
setNextRunAt(
|
||||
typeof responseData.nextRunAt === 'string'
|
||||
? responseData.nextRunAt
|
||||
: responseData.nextRunAt.toISOString?.() || responseData.nextRunAt
|
||||
)
|
||||
}
|
||||
|
||||
// Set schedule status to true when we've successfully saved
|
||||
setScheduleStatus(true)
|
||||
|
||||
// Force a refresh after successful save
|
||||
setRefreshCounter((prev) => prev + 1)
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
logger.error('Error saving schedule:', { error })
|
||||
@@ -167,6 +237,18 @@ export function ScheduleConfig({ blockId, subBlockId, isConnecting }: ScheduleCo
|
||||
setLastRanAt(null)
|
||||
setCronExpression(null)
|
||||
|
||||
// Update startWorkflow value to manual to trigger re-render
|
||||
setStartWorkflow('manual')
|
||||
|
||||
// Set active schedule flag to false since we deleted the schedule
|
||||
setScheduleStatus(false)
|
||||
|
||||
// Trigger workflow update to refresh the UI
|
||||
triggerUpdate()
|
||||
|
||||
// Force a refresh after successful delete
|
||||
setRefreshCounter((prev) => prev + 1)
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
logger.error('Error deleting schedule:', { error })
|
||||
@@ -181,47 +263,48 @@ export function ScheduleConfig({ blockId, subBlockId, isConnecting }: ScheduleCo
|
||||
const isScheduleActive = !!scheduleId && !!nextRunAt
|
||||
|
||||
return (
|
||||
<div className="mt-2 space-y-2">
|
||||
<div className="w-full" onClick={(e) => e.stopPropagation()}>
|
||||
{error && <div className="text-sm text-red-500 dark:text-red-400 mb-2">{error}</div>}
|
||||
|
||||
{isLoading ? (
|
||||
<Button variant="outline" size="sm" className="w-full" disabled={true}>
|
||||
Checking schedule...
|
||||
</Button>
|
||||
<div className="flex items-center justify-center py-2">
|
||||
<div className="h-5 w-5 animate-spin rounded-full border-[1.5px] border-current border-t-transparent" />
|
||||
</div>
|
||||
) : isScheduleActive ? (
|
||||
<div className="bg-green-50 dark:bg-green-950/30 border border-green-200 dark:border-green-800 rounded-md p-2 relative">
|
||||
<div className="flex items-center mb-1">
|
||||
<Calendar className="h-4 w-4 mr-2 text-green-600 dark:text-green-400" />
|
||||
<span className="text-sm font-medium text-green-700 dark:text-green-400">
|
||||
Schedule Active
|
||||
</span>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="ml-auto bg-green-100 text-green-700 border-green-200 dark:bg-green-900 dark:text-green-300 dark:border-green-800"
|
||||
<div className="flex flex-col space-y-2 mb-2">
|
||||
<div className="flex items-center justify-between px-3 py-2 rounded border border-border bg-background">
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<div className="flex-1 truncate">{getScheduleInfo()}</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 shrink-0"
|
||||
onClick={handleOpenModal}
|
||||
disabled={isDeleting || isConnecting}
|
||||
>
|
||||
Active
|
||||
</Badge>
|
||||
{isDeleting ? (
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent" />
|
||||
) : (
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
{getScheduleInfo()}
|
||||
</div>
|
||||
) : (
|
||||
<Button variant="outline" size="sm" className="w-full" disabled={isConnecting}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full h-10 text-sm font-normal bg-background flex items-center"
|
||||
onClick={handleOpenModal}
|
||||
disabled={isConnecting || isSaving || isDeleting}
|
||||
>
|
||||
<Calendar className="h-4 w-4 mr-2" />
|
||||
No schedule configured
|
||||
Configure Schedule
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
onClick={handleOpenModal}
|
||||
disabled={isConnecting || isSaving || isDeleting}
|
||||
>
|
||||
<ExternalLink className="h-4 w-4 mr-2" />
|
||||
{isSaving ? 'Saving...' : isDeleting ? 'Deleting...' : 'Configure Schedule'}
|
||||
</Button>
|
||||
|
||||
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
||||
<ScheduleModal
|
||||
isOpen={isModalOpen}
|
||||
|
||||
@@ -12,9 +12,10 @@ interface TimeInputProps {
|
||||
blockId: string
|
||||
subBlockId: string
|
||||
placeholder?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function TimeInput({ blockId, subBlockId, placeholder }: TimeInputProps) {
|
||||
export function TimeInput({ blockId, subBlockId, placeholder, className }: TimeInputProps) {
|
||||
const [value, setValue] = useSubBlockValue<string>(blockId, subBlockId, true)
|
||||
const [isOpen, setIsOpen] = React.useState(false)
|
||||
|
||||
@@ -79,7 +80,8 @@ export function TimeInput({ blockId, subBlockId, placeholder }: TimeInputProps)
|
||||
variant="outline"
|
||||
className={cn(
|
||||
'w-full justify-start text-left font-normal',
|
||||
!value && 'text-muted-foreground'
|
||||
!value && 'text-muted-foreground',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Clock className="mr-1 h-4 w-4" />
|
||||
|
||||
@@ -137,6 +137,7 @@ export function SubBlock({ blockId, config, isConnecting }: SubBlockProps) {
|
||||
subBlockId={config.id}
|
||||
acceptedTypes={config.acceptedTypes || '*'}
|
||||
multiple={config.multiple === true}
|
||||
maxSize={config.maxSize}
|
||||
/>
|
||||
)
|
||||
case 'webhook-config':
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Info, RectangleHorizontal, RectangleVertical } from 'lucide-react'
|
||||
import { Calendar, Info, RectangleHorizontal, RectangleVertical } from 'lucide-react'
|
||||
import { Handle, NodeProps, Position, useUpdateNodeInternals } from 'reactflow'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -12,7 +12,6 @@ import { mergeSubblockState } from '@/stores/workflows/utils'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
import { BlockConfig, SubBlockConfig } from '@/blocks/types'
|
||||
import { ActionBar } from './components/action-bar/action-bar'
|
||||
import { ScheduleStatus } from './components/action-bar/schedule-status'
|
||||
import { ConnectionBlocks } from './components/connection-blocks/connection-blocks'
|
||||
import { SubBlock } from './components/sub-block/sub-block'
|
||||
|
||||
@@ -46,6 +45,7 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
)
|
||||
const isWide = useWorkflowStore((state) => state.blocks[id]?.isWide ?? false)
|
||||
const blockHeight = useWorkflowStore((state) => state.blocks[id]?.height ?? 0)
|
||||
const hasActiveSchedule = useWorkflowStore((state) => state.hasActiveSchedule ?? false)
|
||||
|
||||
// Workflow store actions
|
||||
const updateBlockName = useWorkflowStore((state) => state.updateBlockName)
|
||||
@@ -194,6 +194,10 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this is a starter block and has an active schedule
|
||||
const isStarterBlock = type === 'starter'
|
||||
const showScheduleIndicator = isStarterBlock && hasActiveSchedule
|
||||
|
||||
return (
|
||||
<div className="relative group">
|
||||
<Card
|
||||
@@ -288,7 +292,21 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
Disabled
|
||||
</Badge>
|
||||
)}
|
||||
{type === 'starter' && <ScheduleStatus blockId={id} />}
|
||||
{/* Schedule indicator badge - displayed for starter blocks with active schedules */}
|
||||
{showScheduleIndicator && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="bg-green-50 text-green-600 border-green-200 animate-pulse-slow"
|
||||
>
|
||||
<Calendar className="h-3 w-3 mr-1" />
|
||||
Scheduled
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">This workflow is running on a schedule</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{config.longDescription && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
||||
@@ -19,6 +19,7 @@ export const FileBlock: BlockConfig<FileParserOutput> = {
|
||||
layout: 'full',
|
||||
acceptedTypes: '.pdf,.csv,.docx',
|
||||
multiple: true,
|
||||
maxSize: 100,
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
|
||||
@@ -29,6 +29,7 @@ const fileUploadBlock: SubBlockConfig = {
|
||||
field: 'inputMethod',
|
||||
value: 'upload',
|
||||
},
|
||||
maxSize: 50,
|
||||
}
|
||||
|
||||
export const MistralParseBlock: BlockConfig<MistralParserOutput> = {
|
||||
|
||||
@@ -115,6 +115,7 @@ export interface SubBlockConfig {
|
||||
// File upload specific properties
|
||||
acceptedTypes?: string
|
||||
multiple?: boolean
|
||||
maxSize?: number
|
||||
}
|
||||
|
||||
// Main block definition
|
||||
|
||||
@@ -625,6 +625,14 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
get().updateLastSaved()
|
||||
workflowSync.sync()
|
||||
},
|
||||
|
||||
setScheduleStatus: (hasActiveSchedule: boolean) => {
|
||||
// Only update if the status has changed to avoid unnecessary rerenders
|
||||
if (get().hasActiveSchedule !== hasActiveSchedule) {
|
||||
set({ hasActiveSchedule })
|
||||
get().updateLastSaved()
|
||||
}
|
||||
},
|
||||
})),
|
||||
{ name: 'workflow-store' }
|
||||
)
|
||||
|
||||
@@ -42,6 +42,7 @@ export interface WorkflowState {
|
||||
isDeployed?: boolean
|
||||
deployedAt?: Date
|
||||
needsRedeployment?: boolean
|
||||
hasActiveSchedule?: boolean
|
||||
}
|
||||
|
||||
export interface WorkflowActions {
|
||||
@@ -50,7 +51,7 @@ export interface WorkflowActions {
|
||||
removeBlock: (id: string) => void
|
||||
addEdge: (edge: Edge) => void
|
||||
removeEdge: (edgeId: string) => void
|
||||
clear: () => void
|
||||
clear: () => Partial<WorkflowState>
|
||||
updateLastSaved: () => void
|
||||
toggleBlockEnabled: (id: string) => void
|
||||
duplicateBlock: (id: string) => void
|
||||
@@ -58,12 +59,13 @@ export interface WorkflowActions {
|
||||
updateBlockName: (id: string, name: string) => void
|
||||
toggleBlockWide: (id: string) => void
|
||||
updateBlockHeight: (id: string, height: number) => void
|
||||
triggerUpdate: () => void
|
||||
updateLoopIterations: (loopId: string, iterations: number) => void
|
||||
updateLoopType: (loopId: string, loopType: Loop['loopType']) => void
|
||||
updateLoopForEachItems: (loopId: string, items: string) => void
|
||||
triggerUpdate: () => void
|
||||
setDeploymentStatus: (isDeployed: boolean, deployedAt?: Date) => void
|
||||
setNeedsRedeploymentFlag: (needsRedeployment: boolean) => void
|
||||
setDeploymentStatus: (isDeployed: boolean, deployedAt?: Date) => void
|
||||
setScheduleStatus: (hasActiveSchedule: boolean) => void
|
||||
}
|
||||
|
||||
export type WorkflowStore = WorkflowState & WorkflowActions
|
||||
|
||||
@@ -113,6 +113,14 @@ export default {
|
||||
'0%,70%,100%': { opacity: '1' },
|
||||
'20%,50%': { opacity: '0' },
|
||||
},
|
||||
'pulse-slow': {
|
||||
'0%, 100%': {
|
||||
opacity: '1',
|
||||
},
|
||||
'50%': {
|
||||
opacity: '0.7',
|
||||
},
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
'slide-down': 'slide-down 0.3s ease-out',
|
||||
@@ -122,6 +130,7 @@ export default {
|
||||
'rocket-pulse': 'rocket-pulse 1.5s ease-in-out infinite',
|
||||
'run-glow': 'run-glow 2s ease-in-out infinite',
|
||||
'caret-blink': 'caret-blink 1.25s ease-out infinite',
|
||||
'pulse-slow': 'pulse-slow 3s ease-in-out infinite',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user