22
33import { lazy , memo , Suspense , useEffect , useMemo , useRef } from 'react'
44import { createLogger } from '@sim/logger'
5+ import { format } from 'date-fns'
56import { useRouter } from 'next/navigation'
67import { Button , PlayOutline , Skeleton , Tooltip } from '@/components/emcn'
78import {
9+ Calendar ,
810 Download ,
911 FileX ,
1012 Folder as FolderIcon ,
@@ -24,6 +26,7 @@ import {
2426import { canonicalWorkspaceFilePath } from '@/lib/copilot/vfs/path-utils'
2527import { triggerFileDownload } from '@/lib/uploads/client/download'
2628import { getFileExtension , getMimeTypeFromExtension } from '@/lib/uploads/utils/file-utils'
29+ import { parseCronToHumanReadable } from '@/lib/workflows/schedules/utils'
2730import {
2831 FileViewer ,
2932 type PreviewMode ,
@@ -50,6 +53,7 @@ import { useUsageLimits } from '@/app/workspace/[workspaceId]/w/[workflowId]/com
5053import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution'
5154import { useFolders } from '@/hooks/queries/folders'
5255import { useLogDetail } from '@/hooks/queries/logs'
56+ import { useWorkspaceSchedules } from '@/hooks/queries/schedules'
5357import { downloadTableExport } from '@/hooks/queries/tables'
5458import { useWorkflows } from '@/hooks/queries/workflows'
5559import { useWorkspaceFiles } from '@/hooks/queries/workspace-files'
@@ -182,6 +186,15 @@ export const ResourceContent = memo(function ResourceContent({
182186 case 'folder' :
183187 return < EmbeddedFolder key = { resource . id } workspaceId = { workspaceId } folderId = { resource . id } />
184188
189+ case 'scheduledtask' :
190+ return (
191+ < EmbeddedScheduledTask
192+ key = { resource . id }
193+ workspaceId = { workspaceId }
194+ scheduleId = { resource . id }
195+ />
196+ )
197+
185198 case 'log' :
186199 return (
187200 < EmbeddedLog
@@ -233,6 +246,8 @@ export function ResourceActions({ workspaceId, resource }: ResourceActionsProps)
233246 )
234247 case 'log' :
235248 return < EmbeddedLogActions workspaceId = { workspaceId } logId = { resource . id } />
249+ case 'scheduledtask' :
250+ return < EmbeddedScheduledTaskActions workspaceId = { workspaceId } />
236251 case 'folder' :
237252 case 'generic' :
238253 return null
@@ -647,6 +662,141 @@ function EmbeddedFolder({ workspaceId, folderId }: EmbeddedFolderProps) {
647662 )
648663}
649664
665+ const SCHEDULE_STATUS_LABEL : Record < string , string > = {
666+ active : 'Active' ,
667+ disabled : 'Paused' ,
668+ completed : 'Completed' ,
669+ }
670+
671+ function formatScheduleInstant ( iso : string | null ) : string {
672+ if ( ! iso ) return '—'
673+ const date = new Date ( iso )
674+ return Number . isNaN ( date . getTime ( ) ) ? '—' : format ( date , "EEE, MMM d 'at' h:mm a" )
675+ }
676+
677+ interface ScheduledTaskFieldProps {
678+ title : string
679+ value : string
680+ }
681+
682+ function ScheduledTaskField ( { title, value } : ScheduledTaskFieldProps ) {
683+ return (
684+ < div className = 'flex flex-col gap-1' >
685+ < span className = 'text-[var(--text-muted)] text-caption' > { title } </ span >
686+ < span className = 'text-[var(--text-body)] text-small' > { value } </ span >
687+ </ div >
688+ )
689+ }
690+
691+ interface EmbeddedScheduledTaskProps {
692+ workspaceId : string
693+ scheduleId : string
694+ }
695+
696+ function EmbeddedScheduledTask ( { workspaceId, scheduleId } : EmbeddedScheduledTaskProps ) {
697+ const { data : schedules = [ ] , isLoading } = useWorkspaceSchedules ( workspaceId )
698+ const schedule = useMemo (
699+ ( ) => schedules . find ( ( s ) => s . id === scheduleId ) ,
700+ [ schedules , scheduleId ]
701+ )
702+
703+ if ( isLoading && ! schedule ) return LOADING_SKELETON
704+
705+ if ( ! schedule ) {
706+ return (
707+ < div className = 'flex h-full flex-col items-center justify-center gap-3' >
708+ < Calendar className = 'size-[32px] text-[var(--text-icon)]' />
709+ < div className = 'flex flex-col items-center gap-1' >
710+ < h2 className = 'font-medium text-[20px] text-[var(--text-primary)]' >
711+ Scheduled task not found
712+ </ h2 >
713+ < p className = 'text-[var(--text-body)] text-small' >
714+ This scheduled task may have been deleted
715+ </ p >
716+ </ div >
717+ </ div >
718+ )
719+ }
720+
721+ const title = schedule . jobTitle || schedule . prompt || 'Scheduled task'
722+ const timing = schedule . cronExpression
723+ ? parseCronToHumanReadable ( schedule . cronExpression , schedule . timezone )
724+ : 'Runs once'
725+ const status = SCHEDULE_STATUS_LABEL [ schedule . status ] ?? schedule . status
726+
727+ return (
728+ < div className = 'flex h-full flex-col gap-6 overflow-y-auto p-6' >
729+ < div className = 'flex items-center gap-2' >
730+ < Calendar className = 'size-[16px] flex-shrink-0 text-[var(--text-icon)]' />
731+ < h2 className = 'truncate font-medium text-[16px] text-[var(--text-primary)]' > { title } </ h2 >
732+ </ div >
733+
734+ < div className = 'grid grid-cols-2 gap-4' >
735+ < ScheduledTaskField title = 'Status' value = { status } />
736+ < ScheduledTaskField title = 'Schedule' value = { timing } />
737+ < ScheduledTaskField title = 'Next run' value = { formatScheduleInstant ( schedule . nextRunAt ) } />
738+ < ScheduledTaskField title = 'Last run' value = { formatScheduleInstant ( schedule . lastRanAt ) } />
739+ </ div >
740+
741+ < div className = 'flex flex-col gap-1' >
742+ < span className = 'text-[var(--text-muted)] text-caption' > Prompt</ span >
743+ < p className = 'whitespace-pre-wrap text-[var(--text-body)] text-small' >
744+ { schedule . prompt || '—' }
745+ </ p >
746+ </ div >
747+
748+ { schedule . jobHistory && schedule . jobHistory . length > 0 && (
749+ < div className = 'flex flex-col gap-2' >
750+ < span className = 'text-[var(--text-muted)] text-caption' > Recent runs</ span >
751+ < div className = 'flex flex-col gap-2' >
752+ { schedule . jobHistory . slice ( 0 , 5 ) . map ( ( run ) => (
753+ < div
754+ key = { run . timestamp }
755+ className = 'flex flex-col gap-1 rounded-[6px] bg-[var(--surface-4)] px-3 py-2'
756+ >
757+ < span className = 'text-[var(--text-tertiary)] text-caption' >
758+ { formatScheduleInstant ( run . timestamp ) }
759+ </ span >
760+ < span className = 'text-[var(--text-body)] text-small' > { run . summary } </ span >
761+ </ div >
762+ ) ) }
763+ </ div >
764+ </ div >
765+ ) }
766+ </ div >
767+ )
768+ }
769+
770+ interface EmbeddedScheduledTaskActionsProps {
771+ workspaceId : string
772+ }
773+
774+ function EmbeddedScheduledTaskActions ( { workspaceId } : EmbeddedScheduledTaskActionsProps ) {
775+ const router = useRouter ( )
776+
777+ const handleOpenScheduledTasks = ( ) => {
778+ router . push ( `/workspace/${ workspaceId } /scheduled-tasks` )
779+ }
780+
781+ return (
782+ < Tooltip . Root >
783+ < Tooltip . Trigger asChild >
784+ < Button
785+ variant = 'subtle'
786+ onClick = { handleOpenScheduledTasks }
787+ className = { RESOURCE_TAB_ICON_BUTTON_CLASS }
788+ aria-label = 'Open in scheduled tasks'
789+ >
790+ < SquareArrowUpRight className = { RESOURCE_TAB_ICON_CLASS } />
791+ </ Button >
792+ </ Tooltip . Trigger >
793+ < Tooltip . Content side = 'bottom' >
794+ < p > Open in scheduled tasks</ p >
795+ </ Tooltip . Content >
796+ </ Tooltip . Root >
797+ )
798+ }
799+
650800interface EmbeddedLogProps {
651801 workspaceId : string
652802 logId : string
0 commit comments