diff --git a/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts b/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts index 9feef89bf3..2130511a97 100644 --- a/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts +++ b/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts @@ -21,12 +21,13 @@ export async function POST( ) { const { workflowId, executionId, contextId } = await params + // Allow resume from dashboard without requiring deployment const access = await validateWorkflowAccess(request, workflowId, false) if (access.error) { return NextResponse.json({ error: access.error.message }, { status: access.error.status }) } - const workflow = access.workflow! + const workflow = access.workflow let payload: any = {} try { @@ -148,6 +149,7 @@ export async function GET( ) { const { workflowId, executionId, contextId } = await params + // Allow access without API key for browser-based UI (same as parent execution endpoint) const access = await validateWorkflowAccess(request, workflowId, false) if (access.error) { return NextResponse.json({ error: access.error.message }, { status: access.error.status }) diff --git a/apps/sim/app/resume/[workflowId]/[executionId]/resume-page-client.tsx b/apps/sim/app/resume/[workflowId]/[executionId]/resume-page-client.tsx index d96b890b3d..0c0a5ece36 100644 --- a/apps/sim/app/resume/[workflowId]/[executionId]/resume-page-client.tsx +++ b/apps/sim/app/resume/[workflowId]/[executionId]/resume-page-client.tsx @@ -1,10 +1,23 @@ 'use client' import { useCallback, useEffect, useMemo, useState } from 'react' +import { RefreshCw } from 'lucide-react' import { useRouter } from 'next/navigation' -import { Badge, Button, Textarea } from '@/components/emcn' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' +import { + Badge, + Button, + Code, + Input, + Label, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, + Textarea, + Tooltip, +} from '@/components/emcn' import { Select, SelectContent, @@ -12,9 +25,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select' -import { Separator } from '@/components/ui/separator' import { useBrandConfig } from '@/lib/branding/branding' -import { cn } from '@/lib/core/utils/cn' import Nav from '@/app/(landing)/components/nav/nav' import type { ResumeStatus } from '@/executor/types' @@ -60,7 +71,8 @@ interface ResumeQueueEntrySummary { interface PausePointWithQueue { contextId: string - triggerBlockId: string + triggerBlockId?: string + blockId?: string response: any registeredAt: string resumeStatus: ResumeStatus @@ -105,14 +117,12 @@ interface ResumeExecutionPageProps { initialContextId?: string | null } -const RESUME_STATUS_STYLES: Record = { - paused: 'border-[var(--c-F59E0B)]/30 bg-[var(--c-F59E0B)]/10 text-[var(--c-F59E0B)]', - queued: - 'border-[var(--brand-tertiary)]/30 bg-[var(--brand-tertiary)]/10 text-[var(--brand-tertiary)]', - resuming: - 'border-[var(--brand-primary)]/30 bg-[var(--brand-primary)]/10 text-[var(--brand-primary)]', - resumed: 'border-[var(--text-success)]/30 bg-[var(--text-success)]/10 text-[var(--text-success)]', - failed: 'border-[var(--text-error)]/30 bg-[var(--text-error)]/10 text-[var(--text-error)]', +const STATUS_BADGE_VARIANT: Record = { + paused: 'orange', + queued: 'blue', + resuming: 'blue', + resumed: 'green', + failed: 'red', } function formatDate(value: string | null): string { @@ -129,17 +139,31 @@ function getStatusLabel(status: string): string { return status.charAt(0).toUpperCase() + status.slice(1) } -function ResumeStatusBadge({ status }: { status: string }) { - const style = - RESUME_STATUS_STYLES[status] ?? - 'border-[var(--border)] bg-[var(--surface-2)] text-[var(--text-secondary)]' +function StatusBadge({ status }: { status: string }) { return ( - + {getStatusLabel(status)} ) } +function getBlockNameFromSnapshot( + executionSnapshot: { snapshot?: string } | null | undefined, + blockId: string | undefined +): string | null { + if (!executionSnapshot?.snapshot || !blockId) return null + try { + const parsed = JSON.parse(executionSnapshot.snapshot) + const workflowState = parsed?.workflow + if (!workflowState?.blocks || !Array.isArray(workflowState.blocks)) return null + // Blocks are stored as an array of serialized blocks with id and metadata.name + const block = workflowState.blocks.find((b: { id: string }) => b.id === blockId) + return block?.metadata?.name || null + } catch { + return null + } +} + export default function ResumeExecutionPage({ params, initialExecutionDetail, @@ -152,9 +176,6 @@ export default function ResumeExecutionPage({ const [executionDetail, setExecutionDetail] = useState( initialExecutionDetail ) - const totalPauses = executionDetail?.totalPauseCount ?? 0 - const resumedCount = executionDetail?.resumedCount ?? 0 - const pendingCount = Math.max(0, totalPauses - resumedCount) const pausePoints = executionDetail?.pausePoints ?? [] const defaultContextId = useMemo(() => { @@ -164,20 +185,6 @@ export default function ResumeExecutionPage({ pausePoints[0]?.contextId ) }, [initialContextId, pausePoints]) - const actionablePausePoints = useMemo( - () => pausePoints.filter((point) => point.resumeStatus === 'paused'), - [pausePoints] - ) - - const groupedPausePoints = useMemo(() => { - const activeStatuses = new Set(['paused', 'queued', 'resuming']) - const resolvedStatuses = new Set(['resumed', 'failed']) - - return { - active: pausePoints.filter((point) => activeStatuses.has(point.resumeStatus)), - resolved: pausePoints.filter((point) => resolvedStatuses.has(point.resumeStatus)), - } - }, [pausePoints]) const [selectedContextId, setSelectedContextId] = useState( defaultContextId ?? null @@ -201,44 +208,32 @@ export default function ResumeExecutionPage({ const normalizeInputFormatFields = useCallback((raw: any): NormalizedInputField[] => { if (!Array.isArray(raw)) return [] - return raw .map((field: any, index: number) => { if (!field || typeof field !== 'object') return null - const name = typeof field.name === 'string' ? field.name.trim() : '' if (!name) return null - - const id = typeof field.id === 'string' && field.id.length > 0 ? field.id : `field_${index}` - const label = - typeof field.label === 'string' && field.label.trim().length > 0 - ? field.label.trim() - : name - const type = - typeof field.type === 'string' && field.type.trim().length > 0 ? field.type : 'string' - const description = - typeof field.description === 'string' && field.description.trim().length > 0 - ? field.description.trim() - : undefined - const placeholder = - typeof field.placeholder === 'string' && field.placeholder.trim().length > 0 - ? field.placeholder.trim() - : undefined - const required = field.required === true - const options = Array.isArray(field.options) ? field.options : undefined - const rows = typeof field.rows === 'number' ? field.rows : undefined - return { - id, + id: typeof field.id === 'string' && field.id.length > 0 ? field.id : `field_${index}`, name, - label, - type, - description, - placeholder, + label: + typeof field.label === 'string' && field.label.trim().length > 0 + ? field.label.trim() + : name, + type: + typeof field.type === 'string' && field.type.trim().length > 0 ? field.type : 'string', + description: + typeof field.description === 'string' && field.description.trim().length > 0 + ? field.description.trim() + : undefined, + placeholder: + typeof field.placeholder === 'string' && field.placeholder.trim().length > 0 + ? field.placeholder.trim() + : undefined, value: field.value, - required, - options, - rows, + required: field.required === true, + options: Array.isArray(field.options) ? field.options : undefined, + rows: typeof field.rows === 'number' ? field.rows : undefined, } as NormalizedInputField }) .filter((field): field is NormalizedInputField => field !== null) @@ -246,36 +241,23 @@ export default function ResumeExecutionPage({ const formatValueForInputField = useCallback( (field: NormalizedInputField, value: any): string => { - if (value === undefined || value === null) { - return '' - } - + if (value === undefined || value === null) return '' switch (field.type) { case 'boolean': - if (typeof value === 'boolean') { - return value ? 'true' : 'false' - } + if (typeof value === 'boolean') return value ? 'true' : 'false' if (typeof value === 'string') { const normalized = value.trim().toLowerCase() - if (normalized === 'true' || normalized === 'false') { - return normalized - } + if (normalized === 'true' || normalized === 'false') return normalized } return '' case 'number': - if (typeof value === 'number') { - return Number.isFinite(value) ? String(value) : '' - } - if (typeof value === 'string') { - return value - } + if (typeof value === 'number') return Number.isFinite(value) ? String(value) : '' + if (typeof value === 'string') return value return '' case 'array': case 'object': case 'files': - if (typeof value === 'string') { - return value - } + if (typeof value === 'string') return value try { return JSON.stringify(value, null, 2) } catch { @@ -291,14 +273,11 @@ export default function ResumeExecutionPage({ const buildInitialFormValues = useCallback( (fields: NormalizedInputField[], submission?: Record) => { const initial: Record = {} - for (const field of fields) { const candidate = submission && Object.hasOwn(submission, field.name) ? submission[field.name] : field.value - initial[field.name] = formatValueForInputField(field, candidate) } - return initial }, [formatValueForInputField] @@ -318,16 +297,11 @@ export default function ResumeExecutionPage({ const parseFormValue = useCallback( (field: NormalizedInputField, rawValue: string): { value: any; error?: string } => { const value = rawValue ?? '' - switch (field.type) { case 'number': { - if (!value.trim()) { - return { value: null } - } + if (!value.trim()) return { value: null } const numericValue = Number(value) - if (Number.isNaN(numericValue)) { - return { value: null, error: 'Enter a valid number.' } - } + if (Number.isNaN(numericValue)) return { value: null, error: 'Enter a valid number.' } return { value: numericValue } } case 'boolean': { @@ -359,17 +333,13 @@ export default function ResumeExecutionPage({ const handleFormFieldChange = useCallback( (fieldName: string, newValue: string) => { if (!selectedContextId) return - setFormValues((prev) => { const updated = { ...prev, [fieldName]: newValue } setFormValuesByContext((map) => ({ ...map, [selectedContextId]: updated })) return updated }) - setFormErrors((prev) => { - if (!prev[fieldName]) { - return prev - } + if (!prev[fieldName]) return prev const { [fieldName]: _, ...rest } = prev return rest }) @@ -380,7 +350,6 @@ export default function ResumeExecutionPage({ const renderFieldInput = useCallback( (field: NormalizedInputField) => { const value = formValues[field.name] ?? '' - switch (field.type) { case 'boolean': { const selectValue = value === 'true' || value === 'false' ? value : '__unset__' @@ -389,12 +358,8 @@ export default function ResumeExecutionPage({ value={selectValue} onValueChange={(val) => handleFormFieldChange(field.name, val)} > - - + + {!field.required && Not set} @@ -409,7 +374,7 @@ export default function ResumeExecutionPage({ handleFormFieldChange(field.name, event.target.value)} + onChange={(e) => handleFormFieldChange(field.name, e.target.value)} placeholder={field.placeholder ?? 'Enter a number...'} /> ) @@ -419,27 +384,26 @@ export default function ResumeExecutionPage({ return (