From 479b4412c8f1a120591e9462923b74a4ca5af9da Mon Sep 17 00:00:00 2001 From: Martin Acosta Date: Fri, 5 Dec 2025 20:08:36 -0300 Subject: [PATCH 1/8] first commit --- bun.lock | 5 + packages/duron-dashboard/package.json | 1 + .../src/components/timeline-modal.tsx | 48 ++++ .../src/components/timeline.tsx | 230 ++++++++++++++++++ packages/duron-dashboard/src/lib/duration.ts | 24 ++ .../duron-dashboard/src/views/step-list.tsx | 172 +++++++------ packages/shared-actions/index.ts | 3 + 7 files changed, 406 insertions(+), 77 deletions(-) create mode 100644 packages/duron-dashboard/src/components/timeline-modal.tsx create mode 100644 packages/duron-dashboard/src/components/timeline.tsx create mode 100644 packages/duron-dashboard/src/lib/duration.ts diff --git a/bun.lock b/bun.lock index f89210d..1ed9a91 100644 --- a/bun.lock +++ b/bun.lock @@ -90,6 +90,7 @@ "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.10", "@tanstack/react-table": "^8.21.3", + "@tanstack/react-virtual": "^3.13.12", "@uiw/react-json-view": "2.0.0-alpha.37", "bun-plugin-tailwind": "^0.1.2", "class-variance-authority": "^0.7.1", @@ -649,6 +650,8 @@ "@tanstack/react-table": ["@tanstack/react-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww=="], + "@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.12", "", { "dependencies": { "@tanstack/virtual-core": "3.13.12" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA=="], + "@tanstack/router-core": ["@tanstack/router-core@1.139.12", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.0", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-HCDi4fpnAFeDDogT0C61yd2nJn0FrIyFDhyHG3xJji8emdn8Ni4rfyrN4Av46xKkXTPUGdbsqih45+uuNtunew=="], "@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.139.12", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16", "tiny-invariant": "^1.3.3", "vite": "^7.1.7" }, "peerDependencies": { "@tanstack/router-core": "^1.139.12", "csstype": "^3.0.10", "solid-js": ">=1.9.5" }, "optionalPeers": ["csstype"] }, "sha512-VARlT9alLnROnPsZtHrSZsqYksIdBBQ24yGzEper5K1+1e0fzpcKLnMYLK9cwr//uWA2xmQayznvBnwcTmnUlg=="], @@ -675,6 +678,8 @@ "@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="], + "@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.12", "", {}, "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA=="], + "@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.139.0", "", {}, "sha512-9PImF1d1tovTUIpjFVa0W7Fwj/MHif7BaaczgJJfbv3sDt1Gh+oW9W9uCw9M3ndEJynnp5ZD/TTs0RGubH5ssg=="], "@tediousjs/connection-string": ["@tediousjs/connection-string@0.5.0", "", {}, "sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ=="], diff --git a/packages/duron-dashboard/package.json b/packages/duron-dashboard/package.json index b1d3b09..6acddd0 100644 --- a/packages/duron-dashboard/package.json +++ b/packages/duron-dashboard/package.json @@ -55,6 +55,7 @@ "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.10", "@tanstack/react-table": "^8.21.3", + "@tanstack/react-virtual": "^3.13.12", "@uiw/react-json-view": "2.0.0-alpha.37", "bun-plugin-tailwind": "^0.1.2", "class-variance-authority": "^0.7.1", diff --git a/packages/duron-dashboard/src/components/timeline-modal.tsx b/packages/duron-dashboard/src/components/timeline-modal.tsx new file mode 100644 index 0000000..d12f5bd --- /dev/null +++ b/packages/duron-dashboard/src/components/timeline-modal.tsx @@ -0,0 +1,48 @@ +'use client' + +import { Menu } from 'lucide-react' + +import { Timeline } from '@/components/timeline' +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' +import { useStepsPolling } from '@/hooks/use-steps-polling' +import { useJob, useJobSteps } from '@/lib/api' + +interface TimelineModalProps { + jobId: string | null + open: boolean + onOpenChange: (open: boolean) => void +} + +export function TimelineModal({ jobId, open, onOpenChange }: TimelineModalProps) { + const { data: job, isLoading: jobLoading } = useJob(jobId) + const { data: stepsData, isLoading: stepsLoading } = useJobSteps(jobId, { + page: 1, + pageSize: 1000, // Get all steps for timeline + }) + + // Enable polling for step updates + useStepsPolling(jobId, open) + + const steps = stepsData?.steps ?? [] + const isLoading = jobLoading || stepsLoading + + return ( + + + +
+ + Timeline +
+
+
+ {isLoading ? ( +
Loading timeline...
+ ) : ( + + )} +
+
+
+ ) +} diff --git a/packages/duron-dashboard/src/components/timeline.tsx b/packages/duron-dashboard/src/components/timeline.tsx new file mode 100644 index 0000000..f8a5919 --- /dev/null +++ b/packages/duron-dashboard/src/components/timeline.tsx @@ -0,0 +1,230 @@ +'use client' + +import { useVirtualizer } from '@tanstack/react-virtual' +import { CircleDot, GitBranch } from 'lucide-react' +import { useEffect, useMemo, useRef, useState } from 'react' + +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import type { Job, JobStep } from '@/lib/api' +import { calculateDurationSeconds, formatDurationSeconds } from '@/lib/duration' + +interface TimelineItem { + id: string + name: string + type: 'job' | 'step' + startedAt: Date | string | number | null + finishedAt: Date | string | number | null | undefined + status: string + level: number +} + +interface TimelineProps { + job: Job | null + steps: Omit[] +} + +const ROW_HEIGHT = 48 + +export function Timeline({ job, steps }: TimelineProps) { + const parentRef = useRef(null) + + // Build timeline items from job and steps + const timelineItems = useMemo(() => { + if (!job) { + return [] + } + + const items: TimelineItem[] = [] + + // Add job as root item + items.push({ + id: job.id, + name: job.actionName, + type: 'job', + startedAt: job.startedAt, + finishedAt: job.finishedAt, + status: job.status, + level: 0, + }) + + // Add steps as children + const sortedSteps = [...steps].sort((a, b) => { + const aStart = a.startedAt ? new Date(a.startedAt).getTime() : 0 + const bStart = b.startedAt ? new Date(b.startedAt).getTime() : 0 + return aStart - bStart + }) + + sortedSteps.forEach((step) => { + items.push({ + id: step.id, + name: step.name, + type: 'step', + startedAt: step.startedAt, + finishedAt: step.finishedAt, + status: step.status, + level: 1, + }) + }) + + return items + }, [job, steps]) + + // Calculate timeline bounds (earliest start, latest end) + const timelineBounds = useMemo(() => { + if (timelineItems.length === 0 || !job?.startedAt) { + return { startTime: 0, endTime: 1, totalDuration: 1 } + } + + const jobStartTime = new Date(job.startedAt).getTime() + let earliestStart = jobStartTime + let latestEnd = jobStartTime + + timelineItems.forEach((item) => { + if (item.startedAt) { + const startTime = new Date(item.startedAt).getTime() + if (startTime < earliestStart) { + earliestStart = startTime + } + + const endTime = item.finishedAt ? new Date(item.finishedAt).getTime() : Date.now() + if (endTime > latestEnd) { + latestEnd = endTime + } + } + }) + + const totalDuration = (latestEnd - earliestStart) / 1000 // Convert to seconds + return { + startTime: earliestStart, + endTime: latestEnd, + totalDuration: totalDuration || 1, + } + }, [timelineItems, job]) + + const virtualizer = useVirtualizer({ + count: timelineItems.length, + getScrollElement: () => parentRef.current, + estimateSize: () => ROW_HEIGHT, + overscan: 10, + }) + + // Update durations for active items + const [_, setNow] = useState(Date.now()) + useEffect(() => { + const hasActiveItems = timelineItems.some((item) => item.startedAt && !item.finishedAt && item.status === 'active') + if (!hasActiveItems) { + return + } + + const interval = setInterval(() => { + setNow(Date.now()) + }, 100) + + return () => clearInterval(interval) + }, [timelineItems]) + + if (timelineItems.length === 0) { + return ( +
No timeline data available
+ ) + } + + return ( +
+
+
+ {virtualizer.getVirtualItems().map((virtualItem) => { + const item = timelineItems[virtualItem.index]! + const duration = calculateDurationSeconds(item.startedAt, item.finishedAt) + const isActive = item.startedAt && !item.finishedAt && item.status === 'active' + + // Calculate relative position and width + let leftPercentage = 0 + let widthPercentage = 0 + + if (item.startedAt && timelineBounds.totalDuration > 0) { + const itemStartTime = new Date(item.startedAt).getTime() + const relativeStart = (itemStartTime - timelineBounds.startTime) / 1000 // seconds from timeline start + leftPercentage = (relativeStart / timelineBounds.totalDuration) * 100 + + // Width is based on duration relative to total timeline duration + widthPercentage = (duration / timelineBounds.totalDuration) * 100 + + // Ensure bar doesn't go outside bounds + if (leftPercentage < 0) { + widthPercentage += leftPercentage + leftPercentage = 0 + } + if (leftPercentage + widthPercentage > 100) { + widthPercentage = 100 - leftPercentage + } + } + + return ( +
+
+ {/* Left side: Tree structure with icons and labels - fixed width */} +
+ {item.type === 'job' ? ( + + ) : ( + + )} + + + {item.name} + + +

{item.name}

+
+
+
+ + {/* Right side: Duration and progress bar - takes remaining space */} +
+
+ {formatDurationSeconds(duration)} +
+
+ {widthPercentage > 0 && ( +
0 ? 'bg-teal-500/80' : 'bg-muted' + }`} + style={{ + left: `${leftPercentage}%`, + width: `${Math.max(widthPercentage, 0.5)}%`, + }} + /> + )} +
+
+
+
+ ) + })} +
+
+
+ ) +} diff --git a/packages/duron-dashboard/src/lib/duration.ts b/packages/duron-dashboard/src/lib/duration.ts new file mode 100644 index 0000000..139b895 --- /dev/null +++ b/packages/duron-dashboard/src/lib/duration.ts @@ -0,0 +1,24 @@ +/** + * Calculate duration in seconds from start and end timestamps + */ +export function calculateDurationSeconds( + startedAt: Date | string | number | null | undefined, + finishedAt: Date | string | number | null | undefined, +): number { + if (!startedAt) { + return 0 + } + const startTime = new Date(startedAt).getTime() + const endTime = finishedAt ? new Date(finishedAt).getTime() : Date.now() + return (endTime - startTime) / 1000 +} + +/** + * Format duration in seconds to a readable string (e.g., "5.442 s") + */ +export function formatDurationSeconds(seconds: number): string { + if (seconds === 0) { + return '0.000 s' + } + return `${seconds.toFixed(3)} s` +} diff --git a/packages/duron-dashboard/src/views/step-list.tsx b/packages/duron-dashboard/src/views/step-list.tsx index 697e66c..2e85352 100644 --- a/packages/duron-dashboard/src/views/step-list.tsx +++ b/packages/duron-dashboard/src/views/step-list.tsx @@ -1,9 +1,11 @@ 'use client' -import { Search } from 'lucide-react' +import { Clock, Search } from 'lucide-react' import { useCallback, useState } from 'react' +import { TimelineModal } from '@/components/timeline-modal' import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion' +import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area' import { useDebouncedCallback } from '@/hooks/use-debounced-callback' @@ -22,6 +24,7 @@ export function StepList({ jobId, selectedStepId, onStepSelect }: StepListProps) const [inputValue, setInputValue] = useState('') const [searchTerm, setSearchTerm] = useState('') const [page, setPage] = useState(1) + const [timelineOpen, setTimelineOpen] = useState(false) const pageSize = 20 // Debounce the search term update with 1000ms delay @@ -56,86 +59,101 @@ export function StepList({ jobId, selectedStepId, onStepSelect }: StepListProps) const steps = stepsData?.steps ?? [] return ( -
-
-
- - handleSearchChange(e.target.value)} - className="pl-8" - /> -
-
- - -
- {stepsLoading ? ( -
Loading steps...
- ) : steps.length === 0 ? ( -
- {inputValue ? 'No steps found matching your search' : 'No steps found'} + <> +
+
+
+
+ + handleSearchChange(e.target.value)} + className="pl-8" + />
- ) : ( - setTimelineOpen(true)} + className="shrink-0" + title="View Timeline" > - {steps.map((step, index) => { - const stepNumber = (page - 1) * pageSize + index + 1 - return ( - - -
-
- #{stepNumber} - {step.name} -
- -
-
- - - -
- ) - })} -
- )} + + Timeline + +
+
- {stepsData && stepsData.total > pageSize && ( -
-
- Showing {(page - 1) * pageSize + 1} - {Math.min(page * pageSize, stepsData.total)} of {stepsData.total}{' '} - steps + +
+ {stepsLoading ? ( +
Loading steps...
+ ) : steps.length === 0 ? ( +
+ {inputValue ? 'No steps found matching your search' : 'No steps found'}
-
- - + ) : ( + + {steps.map((step, index) => { + const stepNumber = (page - 1) * pageSize + index + 1 + return ( + + +
+
+ #{stepNumber} + {step.name} +
+ +
+
+ + + +
+ ) + })} +
+ )} + + {stepsData && stepsData.total > pageSize && ( +
+
+ Showing {(page - 1) * pageSize + 1} - {Math.min(page * pageSize, stepsData.total)} of{' '} + {stepsData.total} steps +
+
+ + +
-
- )} -
- -
-
+ )} +
+ + +
+ + ) } diff --git a/packages/shared-actions/index.ts b/packages/shared-actions/index.ts index 1c6a517..ae59c1a 100644 --- a/packages/shared-actions/index.ts +++ b/packages/shared-actions/index.ts @@ -243,6 +243,9 @@ export const getWeather = defineAction()({ const niceMessage = await ctx.step( `generate nice message`, async ({ signal }) => { + await new Promise((resolve) => { + const timeout = setTimeout(resolve, 30_000) + }) return ctx.var.generateText( { prompt: `Generate a nice message for the weather in ${city} based on the following weather data: ${JSON.stringify(weather)}`, From 0b1bb868184f88e6e4dde8ea2c3581bc09ff266c Mon Sep 17 00:00:00 2001 From: Martin Acosta Date: Fri, 5 Dec 2025 20:25:46 -0300 Subject: [PATCH 2/8] first commit --- .../src/components/timeline-modal.tsx | 46 ++++++- .../src/components/timeline.tsx | 125 ++++++++++-------- 2 files changed, 113 insertions(+), 58 deletions(-) diff --git a/packages/duron-dashboard/src/components/timeline-modal.tsx b/packages/duron-dashboard/src/components/timeline-modal.tsx index d12f5bd..006e3ef 100644 --- a/packages/duron-dashboard/src/components/timeline-modal.tsx +++ b/packages/duron-dashboard/src/components/timeline-modal.tsx @@ -1,11 +1,15 @@ 'use client' import { Menu } from 'lucide-react' +import { useState } from 'react' import { Timeline } from '@/components/timeline' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' +import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area' +import { Separator } from '@/components/ui/separator' import { useStepsPolling } from '@/hooks/use-steps-polling' import { useJob, useJobSteps } from '@/lib/api' +import { StepDetailsContent } from '@/views/step-details-content' interface TimelineModalProps { jobId: string | null @@ -14,6 +18,7 @@ interface TimelineModalProps { } export function TimelineModal({ jobId, open, onOpenChange }: TimelineModalProps) { + const [selectedStepId, setSelectedStepId] = useState(null) const { data: job, isLoading: jobLoading } = useJob(jobId) const { data: stepsData, isLoading: stepsLoading } = useJobSteps(jobId, { page: 1, @@ -26,8 +31,16 @@ export function TimelineModal({ jobId, open, onOpenChange }: TimelineModalProps) const steps = stepsData?.steps ?? [] const isLoading = jobLoading || stepsLoading + // Reset selected step when modal closes + const handleOpenChange = (newOpen: boolean) => { + if (!newOpen) { + setSelectedStepId(null) + } + onOpenChange(newOpen) + } + return ( - +
@@ -35,11 +48,32 @@ export function TimelineModal({ jobId, open, onOpenChange }: TimelineModalProps) Timeline
-
- {isLoading ? ( -
Loading timeline...
- ) : ( - +
+ {/* Timeline Section */} +
+ {isLoading ? ( +
Loading timeline...
+ ) : ( + + )} +
+ + {/* Step Details Section */} + {selectedStepId && ( + <> + + +
+ +
+ +
+ )}
diff --git a/packages/duron-dashboard/src/components/timeline.tsx b/packages/duron-dashboard/src/components/timeline.tsx index f8a5919..bed23f9 100644 --- a/packages/duron-dashboard/src/components/timeline.tsx +++ b/packages/duron-dashboard/src/components/timeline.tsx @@ -21,11 +21,13 @@ interface TimelineItem { interface TimelineProps { job: Job | null steps: Omit[] + selectedStepId?: string | null + onStepSelect?: (stepId: string) => void } const ROW_HEIGHT = 48 -export function Timeline({ job, steps }: TimelineProps) { +export function Timeline({ job, steps, selectedStepId, onStepSelect }: TimelineProps) { const parentRef = useRef(null) // Build timeline items from job and steps @@ -166,62 +168,81 @@ export function Timeline({ job, steps }: TimelineProps) { } } - return ( -
-
- {/* Left side: Tree structure with icons and labels - fixed width */} -
- {item.type === 'job' ? ( - - ) : ( - - )} - - - {item.name} - - -

{item.name}

-
-
-
+ const isSelected = item.type === 'step' && item.id === selectedStepId + const isClickable = item.type === 'step' && onStepSelect + + const content = ( +
+ {/* Left side: Tree structure with icons and labels - fixed width */} +
+ {item.type === 'job' ? ( + + ) : ( + + )} + + + {item.name} + + +

{item.name}

+
+
+
- {/* Right side: Duration and progress bar - takes remaining space */} -
-
- {formatDurationSeconds(duration)} -
-
- {widthPercentage > 0 && ( -
0 ? 'bg-teal-500/80' : 'bg-muted' - }`} - style={{ - left: `${leftPercentage}%`, - width: `${Math.max(widthPercentage, 0.5)}%`, - }} - /> - )} -
+ {/* Right side: Duration and progress bar - takes remaining space */} +
+
+ {formatDurationSeconds(duration)} +
+
+ {widthPercentage > 0 && ( +
0 ? 'bg-teal-500/80' : 'bg-muted' + }`} + style={{ + left: `${leftPercentage}%`, + width: `${Math.max(widthPercentage, 0.5)}%`, + }} + /> + )}
) + + const containerStyle = { + position: 'absolute' as const, + top: 0, + left: 0, + width: '100%', + height: `${virtualItem.size}px`, + transform: `translateY(${virtualItem.start}px)`, + } + + const containerClassName = `flex items-center border-b border-border/50 transition-colors ${ + isSelected ? 'bg-muted' : 'hover:bg-muted/50' + }` + + return isClickable ? ( + + ) : ( +
+ {content} +
+ ) })}
From 35fe97109203f84200b0175eb14b29d6bb8ac4a1 Mon Sep 17 00:00:00 2001 From: Martin Acosta Date: Fri, 5 Dec 2025 20:45:07 -0300 Subject: [PATCH 3/8] first commit --- .../src/components/timeline-modal.tsx | 12 +++++-- .../duron-dashboard/src/views/step-list.tsx | 10 +++--- packages/shared-actions/index.ts | 35 +++++++++++++++++-- 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/packages/duron-dashboard/src/components/timeline-modal.tsx b/packages/duron-dashboard/src/components/timeline-modal.tsx index 006e3ef..f9737d6 100644 --- a/packages/duron-dashboard/src/components/timeline-modal.tsx +++ b/packages/duron-dashboard/src/components/timeline-modal.tsx @@ -39,6 +39,11 @@ export function TimelineModal({ jobId, open, onOpenChange }: TimelineModalProps) onOpenChange(newOpen) } + // Toggle step selection - if clicking the same step, deselect it + const handleStepSelect = (stepId: string) => { + setSelectedStepId((current) => (current === stepId ? null : stepId)) + } + return ( @@ -50,7 +55,7 @@ export function TimelineModal({ jobId, open, onOpenChange }: TimelineModalProps)
{/* Timeline Section */} -
+ {isLoading ? (
Loading timeline...
) : ( @@ -58,10 +63,11 @@ export function TimelineModal({ jobId, open, onOpenChange }: TimelineModalProps) job={job ?? null} steps={steps} selectedStepId={selectedStepId} - onStepSelect={setSelectedStepId} + onStepSelect={handleStepSelect} /> )} -
+ + {/* Step Details Section */} {selectedStepId && ( diff --git a/packages/duron-dashboard/src/views/step-list.tsx b/packages/duron-dashboard/src/views/step-list.tsx index 2e85352..934de5a 100644 --- a/packages/duron-dashboard/src/views/step-list.tsx +++ b/packages/duron-dashboard/src/views/step-list.tsx @@ -105,11 +105,11 @@ export function StepList({ jobId, selectedStepId, onStepSelect }: StepListProps) const stepNumber = (page - 1) * pageSize + index + 1 return ( - -
-
- #{stepNumber} - {step.name} + +
+
+ #{stepNumber} + {step.name}
diff --git a/packages/shared-actions/index.ts b/packages/shared-actions/index.ts index ae59c1a..f9ef1a0 100644 --- a/packages/shared-actions/index.ts +++ b/packages/shared-actions/index.ts @@ -243,9 +243,6 @@ export const getWeather = defineAction()({ const niceMessage = await ctx.step( `generate nice message`, async ({ signal }) => { - await new Promise((resolve) => { - const timeout = setTimeout(resolve, 30_000) - }) return ctx.var.generateText( { prompt: `Generate a nice message for the weather in ${city} based on the following weather data: ${JSON.stringify(weather)}`, @@ -260,6 +257,38 @@ export const getWeather = defineAction()({ }, ) + // Create 10 example steps representing parts of this action for illustrative or logging purposes. + + const exampleSteps = [ + { step: 1, description: `Received request for weather in "${city}"` }, + { step: 2, description: `Validated city name input` }, + { step: 3, description: `Started step: get weather for ${city}` }, + { step: 4, description: `Called external weather service API` }, + { step: 5, description: `Received weather data from API` }, + { step: 6, description: `Parsed weather data: ${JSON.stringify(weather)}` }, + { step: 7, description: `Started step: generate nice message` }, + { step: 8, description: `Called AI model with prompt and weather info` }, + { step: 9, description: `Received generated nice message from AI` }, + { step: 10, description: `Returning weather info and nice message to client` }, + ] + + for (const step of exampleSteps) { + await ctx.step( + step.description, + async () => { + await new Promise((resolve) => { + const timeout = setTimeout(resolve, 3_000) + }) + return { + description: step.description, + } + }, + { + expire: 10_000, + }, + ) + } + return { niceMessage: niceMessage.text, info: weather, From 318bc38d30c6cf53b0b3053310a765a6319072d4 Mon Sep 17 00:00:00 2001 From: Martin Acosta Date: Sat, 6 Dec 2025 09:12:39 -0300 Subject: [PATCH 4/8] first commit --- .../duron-dashboard/src/views/step-list.tsx | 129 +++++++++--------- 1 file changed, 65 insertions(+), 64 deletions(-) diff --git a/packages/duron-dashboard/src/views/step-list.tsx b/packages/duron-dashboard/src/views/step-list.tsx index 934de5a..ebf5c2f 100644 --- a/packages/duron-dashboard/src/views/step-list.tsx +++ b/packages/duron-dashboard/src/views/step-list.tsx @@ -86,72 +86,73 @@ export function StepList({ jobId, selectedStepId, onStepSelect }: StepListProps)
- -
- {stepsLoading ? ( -
Loading steps...
- ) : steps.length === 0 ? ( -
- {inputValue ? 'No steps found matching your search' : 'No steps found'} -
- ) : ( - - {steps.map((step, index) => { - const stepNumber = (page - 1) * pageSize + index + 1 - return ( - - -
-
- #{stepNumber} - {step.name} -
- -
-
- - - -
- ) - })} -
- )} - - {stepsData && stepsData.total > pageSize && ( -
-
- Showing {(page - 1) * pageSize + 1} - {Math.min(page * pageSize, stepsData.total)} of{' '} - {stepsData.total} steps +
+ +
+ {stepsLoading ? ( +
Loading steps...
+ ) : steps.length === 0 ? ( +
+ {inputValue ? 'No steps found matching your search' : 'No steps found'}
-
- - -
-
- )} + ) : ( + + {steps.map((step, index) => { + const stepNumber = (page - 1) * pageSize + index + 1 + return ( + + +
+
+ #{stepNumber} + {step.name} +
+ +
+
+ + + +
+ ) + })} +
+ )} +
+ + +
+ {stepsData && stepsData.total > pageSize && ( +
+
+ Showing {(page - 1) * pageSize + 1} - {Math.min(page * pageSize, stepsData.total)} of {stepsData.total}{' '} + steps +
+
+ + +
- - + )}
From 370ea6e7476d933c64fecd92fa6c27e1ce51c657 Mon Sep 17 00:00:00 2001 From: Martin Acosta Date: Sat, 6 Dec 2025 10:34:21 -0300 Subject: [PATCH 5/8] finish adding timeline view --- packages/duron-dashboard/src/components/badge-status.tsx | 9 ++++++++- .../duron-dashboard/src/views/step-details-content.tsx | 2 +- packages/duron-dashboard/src/views/step-list.tsx | 9 ++++----- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/duron-dashboard/src/components/badge-status.tsx b/packages/duron-dashboard/src/components/badge-status.tsx index 7ce65e9..fdc23cc 100644 --- a/packages/duron-dashboard/src/components/badge-status.tsx +++ b/packages/duron-dashboard/src/components/badge-status.tsx @@ -1,6 +1,7 @@ import { Ban, CheckCircle2, Clock, Play, XCircle } from 'lucide-react' import { Badge } from '@/components/ui/badge' +import { cn } from '../lib/utils' const icons = { created: Clock, @@ -18,9 +19,15 @@ const colors = { cancelled: 'bg-yellow-100 text-yellow-800 border-yellow-800', } -export function BadgeStatus({ status }: { status: string }) { +export function BadgeStatus({ status, justIcon = false }: { status: string; justIcon?: boolean }) { const Icon = icons[status as keyof typeof icons] const color = colors[status as keyof typeof colors] + if (justIcon) { + return ( + {Icon && } + ) + } + return ( {Icon && } diff --git a/packages/duron-dashboard/src/views/step-details-content.tsx b/packages/duron-dashboard/src/views/step-details-content.tsx index ed5af9f..978bd3f 100644 --- a/packages/duron-dashboard/src/views/step-details-content.tsx +++ b/packages/duron-dashboard/src/views/step-details-content.tsx @@ -99,7 +99,7 @@ export function StepDetailsContent({ stepId, jobId }: StepDetailsContentProps) {
-
+
Step: {step.name}
diff --git a/packages/duron-dashboard/src/views/step-list.tsx b/packages/duron-dashboard/src/views/step-list.tsx index ebf5c2f..8aa3400 100644 --- a/packages/duron-dashboard/src/views/step-list.tsx +++ b/packages/duron-dashboard/src/views/step-list.tsx @@ -87,7 +87,7 @@ export function StepList({ jobId, selectedStepId, onStepSelect }: StepListProps)
- +
{stepsLoading ? (
Loading steps...
@@ -105,14 +105,14 @@ export function StepList({ jobId, selectedStepId, onStepSelect }: StepListProps) {steps.map((step, index) => { const stepNumber = (page - 1) * pageSize + index + 1 return ( - +
#{stepNumber} - {step.name} + {step.name}
- +
@@ -124,7 +124,6 @@ export function StepList({ jobId, selectedStepId, onStepSelect }: StepListProps) )}
-
{stepsData && stepsData.total > pageSize && ( From 4ae97d7f9cc27470d77b3cd22861aa0b0f734295 Mon Sep 17 00:00:00 2001 From: Martin Acosta Date: Sat, 6 Dec 2025 10:34:41 -0300 Subject: [PATCH 6/8] finish adding timeline view --- packages/duron-dashboard/src/components/ui/tooltip.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/duron-dashboard/src/components/ui/tooltip.tsx b/packages/duron-dashboard/src/components/ui/tooltip.tsx index 375b767..919aea9 100644 --- a/packages/duron-dashboard/src/components/ui/tooltip.tsx +++ b/packages/duron-dashboard/src/components/ui/tooltip.tsx @@ -37,7 +37,7 @@ function TooltipContent({ {...props} > {children} - + ) From e5caf28bf7d26d280b6fe080e77b163a2658ddd5 Mon Sep 17 00:00:00 2001 From: Martin Acosta Date: Sat, 6 Dec 2025 10:34:50 -0300 Subject: [PATCH 7/8] finish adding timeline view --- packages/duron-dashboard/src/views/step-list.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/duron-dashboard/src/views/step-list.tsx b/packages/duron-dashboard/src/views/step-list.tsx index 8aa3400..e11d061 100644 --- a/packages/duron-dashboard/src/views/step-list.tsx +++ b/packages/duron-dashboard/src/views/step-list.tsx @@ -7,7 +7,7 @@ import { TimelineModal } from '@/components/timeline-modal' import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' -import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area' +import { ScrollArea } from '@/components/ui/scroll-area' import { useDebouncedCallback } from '@/hooks/use-debounced-callback' import { useStepsPolling } from '@/hooks/use-steps-polling' import { useJobSteps } from '@/lib/api' From ec2c2b0603c745795ea753222f08335bf1c8039e Mon Sep 17 00:00:00 2001 From: Martin Acosta Date: Sat, 6 Dec 2025 10:35:13 -0300 Subject: [PATCH 8/8] finish adding timeline view --- packages/shared-actions/index.ts | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/packages/shared-actions/index.ts b/packages/shared-actions/index.ts index f9ef1a0..1c6a517 100644 --- a/packages/shared-actions/index.ts +++ b/packages/shared-actions/index.ts @@ -257,38 +257,6 @@ export const getWeather = defineAction()({ }, ) - // Create 10 example steps representing parts of this action for illustrative or logging purposes. - - const exampleSteps = [ - { step: 1, description: `Received request for weather in "${city}"` }, - { step: 2, description: `Validated city name input` }, - { step: 3, description: `Started step: get weather for ${city}` }, - { step: 4, description: `Called external weather service API` }, - { step: 5, description: `Received weather data from API` }, - { step: 6, description: `Parsed weather data: ${JSON.stringify(weather)}` }, - { step: 7, description: `Started step: generate nice message` }, - { step: 8, description: `Called AI model with prompt and weather info` }, - { step: 9, description: `Received generated nice message from AI` }, - { step: 10, description: `Returning weather info and nice message to client` }, - ] - - for (const step of exampleSteps) { - await ctx.step( - step.description, - async () => { - await new Promise((resolve) => { - const timeout = setTimeout(resolve, 3_000) - }) - return { - description: step.description, - } - }, - { - expire: 10_000, - }, - ) - } - return { niceMessage: niceMessage.text, info: weather,