Skip to content

Commit ab0f210

Browse files
committed
feat(dashboard): enhance recent executions display by adding 'endedAt' and 'duration' fields, and implement dropdown actions for viewing execution details
1 parent 3a3364e commit ab0f210

3 files changed

Lines changed: 102 additions & 14 deletions

File tree

apps/api/src/routes/dashboard.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ dashboard.get("/", async (c) => {
7171
workflowName: workflows.find((w) => w.id === e.workflowId)?.name || "",
7272
status: e.status,
7373
startedAt: e.startedAt ? Number(e.startedAt) : Date.now(),
74+
endedAt: e.endedAt ? Number(e.endedAt) : undefined,
7475
}));
7576

7677
const stats: DashboardStats = {

apps/web/src/pages/dashboard-page.tsx

Lines changed: 100 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { CreateWorkflowRequest, WorkflowType } from "@dafthunk/types";
2-
import { formatDistanceToNow } from "date-fns";
3-
import { AlertCircle, Clock, Logs, Plus, Target, Workflow } from "lucide-react";
2+
import { format } from "date-fns";
3+
import { AlertCircle, Clock, Logs, Plus, Target, Workflow, MoreHorizontal, Eye } from "lucide-react";
44
import { useState } from "react";
55
import { useNavigate } from "react-router";
66
import { Link } from "react-router";
@@ -14,9 +14,24 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
1414
import { DataTableCard } from "@/components/ui/data-table-card";
1515
import { CreateWorkflowDialog } from "@/components/workflow/create-workflow-dialog";
1616
import type { WorkflowExecutionStatus } from "@/components/workflow/workflow-types";
17+
import {
18+
DropdownMenu,
19+
DropdownMenuContent,
20+
DropdownMenuItem,
21+
DropdownMenuTrigger,
22+
} from "@/components/ui/dropdown-menu";
1723
import { useDashboard } from "@/services/dashboard-service";
1824
import { createWorkflow } from "@/services/workflow-service";
1925

26+
// Define a type for recent execution items, mirroring DashboardStats.recentExecutions
27+
interface RecentExecutionItem {
28+
id: string;
29+
workflowName: string;
30+
status: string;
31+
startedAt: number;
32+
endedAt?: number;
33+
}
34+
2035
export function DashboardPage() {
2136
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
2237
const navigate = useNavigate();
@@ -175,26 +190,97 @@ export function DashboardPage() {
175190
},
176191
{
177192
accessorKey: "status",
178-
header: "Status",
193+
header: "Execution Status",
179194
cell: ({ row }) => {
180-
const badgeStatus = row.original
181-
.status as WorkflowExecutionStatus;
195+
const execution = row.original as RecentExecutionItem;
196+
const badgeStatus = execution.status as WorkflowExecutionStatus;
182197
return <ExecutionStatusBadge status={badgeStatus} />;
183198
},
184199
},
185200
{
186201
accessorKey: "startedAt",
187-
header: "Started",
188-
cell: ({ row }) => (
189-
<span className="text-right text-xs text-muted-foreground">
190-
{formatDistanceToNow(row.original.startedAt, {
191-
addSuffix: true,
192-
})}
193-
</span>
194-
),
202+
header: "Started At",
203+
cell: ({ row }) => {
204+
const execution = row.original as RecentExecutionItem;
205+
return (
206+
<span className="text-xs text-muted-foreground">
207+
{execution.startedAt ? format(new Date(execution.startedAt), "PPpp") : "-"}
208+
</span>
209+
);
210+
},
211+
},
212+
{
213+
accessorKey: "endedAt",
214+
header: "Ended At",
215+
cell: ({ row }) => {
216+
const execution = row.original as RecentExecutionItem;
217+
const formatted = execution.endedAt
218+
? format(new Date(execution.endedAt), "PPpp")
219+
: "-";
220+
return (
221+
<span className="text-xs text-muted-foreground">
222+
{formatted}
223+
</span>
224+
);
225+
},
226+
},
227+
{
228+
accessorKey: "duration",
229+
header: "Duration",
230+
cell: ({ row }) => {
231+
const execution = row.original as RecentExecutionItem;
232+
const { startedAt, endedAt } = execution;
233+
234+
if (startedAt && endedAt) {
235+
const durationMs =
236+
new Date(endedAt).getTime() - new Date(startedAt).getTime();
237+
const seconds = Math.floor((durationMs / 1000) % 60);
238+
const minutes = Math.floor((durationMs / (1000 * 60)) % 60);
239+
240+
let formattedDuration = "";
241+
if (minutes > 0) {
242+
formattedDuration += `${minutes}m `;
243+
}
244+
formattedDuration += `${seconds}s`;
245+
if (formattedDuration.trim() === "0s" && durationMs > 0 && durationMs < 1000) {
246+
formattedDuration = "<1s";
247+
} else if (formattedDuration.trim() === "0s" && durationMs === 0) {
248+
formattedDuration = "0s";
249+
}
250+
251+
252+
return <div>{formattedDuration.trim()}</div>;
253+
}
254+
return <div>-</div>;
255+
},
256+
},
257+
{
258+
id: "actions",
259+
cell: ({ row }) => {
260+
const execution = row.original as RecentExecutionItem;
261+
return (
262+
<div className="text-right">
263+
<DropdownMenu>
264+
<DropdownMenuTrigger asChild>
265+
<Button variant="ghost" className="h-8 w-8 p-0">
266+
<span className="sr-only">Open menu</span>
267+
<MoreHorizontal className="h-4 w-4" />
268+
</Button>
269+
</DropdownMenuTrigger>
270+
<DropdownMenuContent align="end">
271+
<DropdownMenuItem asChild>
272+
<Link to={`/workflows/executions/${execution.id}`}>
273+
View
274+
</Link>
275+
</DropdownMenuItem>
276+
</DropdownMenuContent>
277+
</DropdownMenu>
278+
</div>
279+
);
280+
},
195281
},
196282
]}
197-
data={dashboardStats.recentExecutions}
283+
data={dashboardStats.recentExecutions as RecentExecutionItem[]}
198284
emptyState={{
199285
title: "No executions",
200286
description: "There are no recent executions to display.",

packages/types/src/dashboard.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface DashboardStats {
1515
workflowName: string;
1616
status: string;
1717
startedAt: number;
18+
endedAt?: number;
1819
}>;
1920
}
2021

0 commit comments

Comments
 (0)