From 7ddafda34e9eab1f60e7df9b23f22b24897de2ab Mon Sep 17 00:00:00 2001 From: mohd-sahil-7 Date: Sat, 22 Aug 2026 10:21:09 +0530 Subject: [PATCH] chore: save latest frontend changes --- docker-compose.full.yml | 10 +- docker_logs.txt | 0 frontend/app/(app)/dashboard/page.tsx | 116 +---- frontend/app/(app)/executions/[id]/page.tsx | 17 +- frontend/app/(app)/executions/page.tsx | 7 +- frontend/app/(app)/workflows/[id]/page.tsx | 3 +- frontend/app/(app)/workflows/page.tsx | 491 +++++++++++------- .../components/dashboard/DashboardView.tsx | 167 ++++++ .../components/dashboard/LiveActivity.tsx | 29 +- .../components/dashboard/QuickActions.tsx | 177 ++++--- .../dashboard/RunningExecutions.tsx | 2 +- .../components/dashboard/SystemHealth.tsx | 23 +- .../execution-details/ActiveLogs.tsx | 81 ++- .../ExecutionDetailsView.tsx | 147 ++++-- .../execution-details/ExecutionHeader.tsx | 60 +-- .../execution-details/ExecutionTimeline.tsx | 103 ++-- .../WorkflowExecutionCanvas.tsx | 10 +- .../executions/ExecutionFiltersBar.tsx | 46 +- .../executions/ExecutionMetrics.tsx | 4 +- .../executions/ExecutionPagination.tsx | 97 ++-- .../components/executions/ExecutionTable.tsx | 255 +++------ .../components/executions/ExecutionsView.tsx | 228 ++++---- .../editor/workflow-canvas.tsx | 130 ++--- .../editor/workflow-properties.tsx | 137 +++-- .../editor/workflow-toolbar.tsx | 393 ++++++++++---- .../workflow-builder/hooks/useAuth.ts | 6 +- .../workflow-builder/nodes/condition-node.tsx | 9 +- .../workflow-builder/nodes/end-node.tsx | 46 ++ .../nodes/http-request-node.tsx | 20 +- .../workflow-builder/nodes/start-node.tsx | 16 +- frontend/lib/api/auth.ts | 15 +- frontend/lib/api/axios.ts | 93 +++- frontend/lib/api/dashboard.ts | 195 ++++++- frontend/lib/api/execution-details.ts | 383 +++++++++++++- frontend/lib/api/executions.ts | 78 ++- frontend/lib/api/workflow.ts | 12 +- frontend/lib/mappers/workflow.mapper.ts | 19 +- frontend/lib/react-flow/node-factory.ts | 13 +- frontend/lib/react-flow/node-library.ts | 7 + frontend/lib/react-flow/node-types.ts | 28 +- frontend/lib/stores/auth-store.ts | 36 +- frontend/lib/stores/workflow-store.ts | 220 ++++---- frontend/mocks/dashboard.ts | 74 --- frontend/mocks/execution-details.ts | 2 + frontend/services/workflow.service.ts | 34 +- frontend/types/dashboard.ts | 10 +- frontend/types/execution-details.ts | 6 +- frontend/types/execution.ts | 65 ++- frontend/types/workflow-definition.ts | 20 + frontend/types/workflow.ts | 2 + .../adapter/out/http/HttpConfig.java | 16 +- .../adapter/out/http/HttpWorker.java | 4 +- .../src/main/resources/application.yaml | 8 +- .../src/main/resources/application.properties | 6 +- 54 files changed, 2766 insertions(+), 1410 deletions(-) create mode 100644 docker_logs.txt create mode 100644 frontend/components/dashboard/DashboardView.tsx create mode 100644 frontend/components/workflow-builder/nodes/end-node.tsx diff --git a/docker-compose.full.yml b/docker-compose.full.yml index 46de4f1..ca121bd 100644 --- a/docker-compose.full.yml +++ b/docker-compose.full.yml @@ -62,8 +62,15 @@ services: DB_URL: jdbc:postgresql://db:5432/flowforge DB_USERNAME: postgres DB_PASSWORD: postgres + KAFKA_BOOTSTRAP_SERVERS: kafka:9092 + EUREKA_URL: http://discovery-service:8761/eureka/ + JAEGER_ENDPOINT: http://jaeger:4318/v1/traces + SPRING_KAFKA_PRODUCER_PROPERTIES_SPRING_JSON_ADD_TYPE_HEADERS: "true" + SPRING_KAFKA_CONSUMER_PROPERTIES_SPRING_JSON_USE_TYPE_HEADERS: "true" + SPRING_KAFKA_CONSUMER_PROPERTIES_SPRING_JSON_TYPE_MAPPING: "com.flowforge.worker_service.adapter.out.kafka.TaskSucceededEvent:com.flowforge.workflowservice.application.execution.event.TaskSucceededEvent,com.flowforge.worker_service.adapter.out.kafka.TaskFailedEvent:com.flowforge.workflowservice.application.execution.event.TaskFailedEvent" volumes: - ./database:/app/database + - ./workflow-service/target/workflow-service-0.0z.1-SNAPSHOT.jar:/app/app.jar worker-service: image: aswinrj/worker-service @@ -80,4 +87,5 @@ services: SLACK_BOT_TOKEN: ${SLACK_BOT_TOKEN} SPRING_PROFILES_ACTIVE: docker volumes: - - ./database/migrations:/app/database/migrations \ No newline at end of file + - ./database/migrations:/app/database/migrations + - ./worker-service/target/worker-service-0.0.1-SNAPSHOT.jar:/app/app.jar \ No newline at end of file diff --git a/docker_logs.txt b/docker_logs.txt new file mode 100644 index 0000000..e69de29 diff --git a/frontend/app/(app)/dashboard/page.tsx b/frontend/app/(app)/dashboard/page.tsx index 19c58d8..51bbb36 100644 --- a/frontend/app/(app)/dashboard/page.tsx +++ b/frontend/app/(app)/dashboard/page.tsx @@ -1,111 +1,5 @@ -import { - Activity, - CheckCircle2, - CircleAlert, - Clock3, - Workflow, -} from "lucide-react"; - -import { getDashboardData } from "@/lib/api/dashboard"; -import StatCard from "@/components/dashboard/StatCard"; -import RunningExecutions from "../../../components/dashboard/RunningExecutions"; -import QuickActions from "../../../components/dashboard/QuickActions"; -import LiveActivity from "../../../components/dashboard/LiveActivity"; -import SystemHealth from "../../../components/dashboard/SystemHealth"; -import { getGreeting } from "@/lib/utils/get-greeting"; -import { formatNumber } from "@/lib/utils/format-number"; - -export default async function Dashboard() { - const dashboard = await getDashboardData(); - const greeting = getGreeting() - - const { stats } = dashboard; - - const statCards = [ - { - title: "Total Workflows", - value: stats.totalWorkflows, - trend: stats.workflowGrowth, - description: "from last week", - icon: Workflow, - }, - { - title: "Running Executions", - value: stats.runningExecutions, - description: "Currently active", - icon: Activity, - }, - { - title: "Completed", - value: formatNumber(stats.completedExecutions), - description: `${stats.successRate}% success rate`, - icon: CheckCircle2, - }, - { - title: "Failed (24h)", - value: stats.failedExecutions24h, - trend: stats.failureChange, - description: "from yesterday", - icon: CircleAlert, - }, - { - title: "Pending Approvals", - value: stats.pendingApprovals, - description: "Requires attention", - icon: Clock3, - }, - ]; - - return ( -
-
- -
-
-

- {greeting}, Sahil 👋 -

- -

- Here's what's happening with your workflows. -

-
-
- -
- {statCards.map((card) => ( - - ))} -
- -
-
- - - -
- -
- - - -
-
- -
-
- ); -} \ No newline at end of file +import DashboardView from "@/components/dashboard/DashboardView"; + +export default function DashboardPage() { + return ; +} \ No newline at end of file diff --git a/frontend/app/(app)/executions/[id]/page.tsx b/frontend/app/(app)/executions/[id]/page.tsx index 81a17f2..601938b 100644 --- a/frontend/app/(app)/executions/[id]/page.tsx +++ b/frontend/app/(app)/executions/[id]/page.tsx @@ -1,7 +1,3 @@ -import { notFound } from "next/navigation"; - -import { getExecutionDetails } from "@/lib/api/execution-details"; - import ExecutionDetailsView from "@/components/execution-details/ExecutionDetailsView"; interface ExecutionDetailsPageProps { @@ -15,16 +11,5 @@ export default async function ExecutionDetailsPage({ }: ExecutionDetailsPageProps) { const { id } = await params; - const execution = - await getExecutionDetails(id); - - if (!execution) { - notFound(); - } - - return ( - - ); + return ; } \ No newline at end of file diff --git a/frontend/app/(app)/executions/page.tsx b/frontend/app/(app)/executions/page.tsx index 0fe3095..a975795 100644 --- a/frontend/app/(app)/executions/page.tsx +++ b/frontend/app/(app)/executions/page.tsx @@ -1,9 +1,6 @@ import ExecutionsView from "@/components/executions/ExecutionsView"; -import { getExecutionsData } from "@/lib/api/executions"; - -export default async function ExecutionsPage() { - const data = await getExecutionsData(); +export default function ExecutionsPage() { return (
@@ -17,7 +14,7 @@ export default async function ExecutionsPage() {

- +
); diff --git a/frontend/app/(app)/workflows/[id]/page.tsx b/frontend/app/(app)/workflows/[id]/page.tsx index a9a8777..7e91a75 100644 --- a/frontend/app/(app)/workflows/[id]/page.tsx +++ b/frontend/app/(app)/workflows/[id]/page.tsx @@ -20,10 +20,9 @@ export default function WorkflowPage() { return; } - const workflow = await workflowService.getById( + const workflow = await workflowService.loadWorkflow( id as string ); - if (workflow) { importWorkflow(workflow); } diff --git a/frontend/app/(app)/workflows/page.tsx b/frontend/app/(app)/workflows/page.tsx index 409f19b..9a8671e 100644 --- a/frontend/app/(app)/workflows/page.tsx +++ b/frontend/app/(app)/workflows/page.tsx @@ -1,201 +1,344 @@ "use client" -import { Compass, Plus, Trash2, Pencil, Play } from "lucide-react" +import { + Compass, + Plus, + Trash2, + Pencil, + Play, + Workflow as WorkflowIcon, + RefreshCw, + ArrowUpDown, + Upload, +} from "lucide-react" import { Button } from "@/components/ui/button" import { Select, SelectTrigger, SelectValue, SelectItem, SelectContent } from "@/components/ui/select"; import Link from "next/link"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useMemo, useCallback } from "react"; import { workflowService } from "@/services/workflow.service"; import { Workflow } from "@/types/workflow"; import { useRouter } from "next/navigation"; -export default function Workflows(){ +function renderStatusBadge(status?: string) { + const upper = (status || "DRAFT").toUpperCase(); + let badgeStyle = "border-zinc-700/40 bg-zinc-800/60 text-zinc-300"; + let dotStyle = "bg-zinc-400"; + let label = "Draft"; - const router = useRouter() + if (upper === "PUBLISHED") { + badgeStyle = "border-emerald-500/20 bg-emerald-500/10 text-emerald-400"; + dotStyle = "bg-emerald-400"; + label = "Published"; + } else if (upper === "ARCHIVED") { + badgeStyle = "border-amber-500/20 bg-amber-500/10 text-amber-400"; + dotStyle = "bg-amber-400"; + label = "Archived"; + } - const filters = [ - "All", - "Draft", - "Running", - "Completed", - "Failed" - ]; + return ( + + + {label} + + ); +} - const [workflows, setWorkflows] = useState([]) +export default function Workflows() { + const router = useRouter() - const handleDelete = async (id:string) => { - await workflowService.delete(id); + const filterTabs = [ + "All", + "Draft", + "Published", + "Archived", + ]; - const data = await workflowService.getAll() + const [workflows, setWorkflows] = useState([]); + const [activeFilter, setActiveFilter] = useState("All"); + const [sortBy, setSortBy] = useState("modified"); + const [isLoading, setIsLoading] = useState(true); + const [isRefreshing, setIsRefreshing] = useState(false); - setWorkflows([...data]) + const loadWorkflows = useCallback(async (showRefreshing = false) => { + if (showRefreshing) { + setIsRefreshing(true); + } else { + setIsLoading(true); } + try { + const data = await workflowService.getAll(); + setWorkflows(data || []); + } catch (err) { + console.error("Failed to load workflows:", err); + } finally { + setIsLoading(false); + setIsRefreshing(false); + } + }, []); - useEffect(()=> { - async function loadWorkflows(){ - const data = await workflowService.getAll() - setWorkflows(data) - } - - loadWorkflows(); - },[]) - - return ( - <> -
-
-
-

Workflows

-

Create, edit and execute workflow automations

-
+ useEffect(() => { + loadWorkflows(); + }, [loadWorkflows]); -
- - - - -
+ const handleDelete = async (id: string) => { + try { + await workflowService.delete(id); + await loadWorkflows(true); + } catch (err) { + console.error("Failed to delete workflow:", err); + } + }; + + const handlePublishWorkflow = async (id: string) => { + try { + await workflowService.publish(id); + await loadWorkflows(true); + } catch (err: any) { + console.error("Failed to publish workflow:", err); + alert( + err?.response?.data?.message || + "Failed to publish workflow. Please ensure node configuration is valid." + ); + } + }; + + const filteredWorkflows = useMemo(() => { + let result = [...workflows]; + + // Filter by Workflow Lifecycle Status (DRAFT, PUBLISHED, ARCHIVED) + if (activeFilter !== "All") { + const target = activeFilter.toUpperCase(); + result = result.filter((w) => { + const s = (w.status || "DRAFT").toUpperCase(); + return s === target; + }); + } + + // Sort workflows + result.sort((a, b) => { + if (sortBy === "modified") { + return new Date(b.updatedAt || 0).getTime() - new Date(a.updatedAt || 0).getTime(); + } + if (sortBy === "created") { + return new Date(b.createdAt || 0).getTime() - new Date(a.createdAt || 0).getTime(); + } + if (sortBy === "a-z") { + return a.name.localeCompare(b.name); + } + if (sortBy === "z-a") { + return b.name.localeCompare(a.name); + } + return 0; + }); + + return result; + }, [workflows, activeFilter, sortBy]); + + return ( + <> +
+
+
+

Workflows

+

Create, edit and execute workflow automations

+
+ +
+ + + + +
+
+ + {/* Filter & Control Bar - Aligned visually with ExecutionsFilterBar */} +
+
+ {/* Status Filter Tabs */} +
+ {filterTabs.map((tab) => ( + + ))}
-
-
-
-
- {filters.map((filter)=>( - - ))} -
-
-
- Sort By: - -
-
- Type: - -
-
+ {/* Sort By Dropdown */} +
+ + Sort By: +
+
-
-
-
Workflow Name
-
Status
-
Last Modified
-
Nodes
-
Execs
-
Actions
-
+ {/* Refresh Action */} + +
- {workflows.length === 0 ? ( -
- No workflows found. -
- ) : ( - workflows.map((workflow) => ( -
- router.push(`/workflows/${workflow.id}`) - } - className="grid grid-cols-[3fr_1.5fr_1.5fr_0.8fr_0.8fr_1fr] items-center border-b border-zinc-900 px-6 py-6 last:border-none cursor-pointer" - > - {/* Workflow */} -
-
- âš¡ -
- -
-

- {workflow.name} -

- -
-
- -
- Draft -
- -
- {new Date(workflow.updatedAt).toLocaleString()} -
- - {/* Nodes */} -
- {workflow.nodes.length} -
- - {/* Execs */} -
-
- - {/* Actions */} -
- - - - - -
-
- )) - )} - - -
+ {/* Workflows Table */} +
+
+
Workflow Name
+
Status
+
Last Modified
+
Nodes
+
Execs
+
Actions
+
-
- + {isLoading ? ( +
+ +

Loading workflows...

+
+ ) : filteredWorkflows.length === 0 ? ( +
+ No workflows match the selected filter.
+ ) : ( + filteredWorkflows.map((workflow) => { + const isPublished = (workflow.status || "").toUpperCase() === "PUBLISHED"; + + return ( +
router.push(`/workflows/${workflow.id}`)} + className="group grid grid-cols-[3fr_1.5fr_1.5fr_0.8fr_0.8fr_1fr] items-center border-b border-border/40 px-6 py-4 last:border-none cursor-pointer transition-colors hover:bg-muted/40" + > + {/* Workflow Name */} +
+
+ +
+ +
+

+ {workflow.name} +

+
+
+ + {/* Status */} +
+ {renderStatusBadge(workflow.status)} +
+ + {/* Last Modified */} +
+ {workflow.updatedAt ? new Date(workflow.updatedAt).toLocaleString() : "-"} +
+ + {/* Nodes */} +
+ - +
+ + {/* Execs */} +
+ - +
+ + {/* Actions */} +
+ + + {!isPublished && ( + + )} + + + + +
+
+ ); + }) + )}
- - ) +
+ + ) } \ No newline at end of file diff --git a/frontend/components/dashboard/DashboardView.tsx b/frontend/components/dashboard/DashboardView.tsx new file mode 100644 index 0000000..62aa8ff --- /dev/null +++ b/frontend/components/dashboard/DashboardView.tsx @@ -0,0 +1,167 @@ +"use client"; + +import { useEffect, useState, useCallback } from "react"; +import { + Activity, + AlertCircle, + CheckCircle2, + CircleAlert, + Clock3, + RefreshCw, + Workflow, +} from "lucide-react"; + +import { getDashboardData } from "@/lib/api/dashboard"; +import type { DashboardData } from "@/types/dashboard"; +import StatCard from "@/components/dashboard/StatCard"; +import RunningExecutions from "@/components/dashboard/RunningExecutions"; +import QuickActions from "@/components/dashboard/QuickActions"; +import LiveActivity from "@/components/dashboard/LiveActivity"; +import SystemHealth from "@/components/dashboard/SystemHealth"; +import { getGreeting } from "@/lib/utils/get-greeting"; +import { formatNumber } from "@/lib/utils/format-number"; +import { Button } from "@/components/ui/button"; + +export default function DashboardView() { + const [dashboard, setDashboard] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchDashboard = useCallback(async () => { + setIsLoading(true); + setError(null); + try { + const data = await getDashboardData(); + setDashboard(data); + } catch (err: any) { + console.error("Failed to load dashboard data:", err); + setError( + err?.response?.data?.message || + err?.message || + "Failed to load dashboard data. Please check your connection or login status." + ); + } finally { + setIsLoading(false); + } + }, []); + + useEffect(() => { + fetchDashboard(); + }, [fetchDashboard]); + + if (isLoading) { + return ( +
+
+ +

Loading dashboard data...

+
+
+ ); + } + + if (error || !dashboard) { + return ( +
+
+ +
+

Failed to Load Dashboard

+

+ {error || "An unexpected error occurred while loading dashboard metrics."} +

+
+ +
+
+ ); + } + + const greeting = getGreeting(); + const { stats } = dashboard; + + const statCards = [ + { + title: "Total Workflows", + value: stats.totalWorkflows, + description: `${stats.createdThisWeek} created this week`, + icon: Workflow, + }, + { + title: "Running Executions", + value: stats.runningExecutions, + description: "Currently active", + icon: Activity, + }, + { + title: "Completed", + value: formatNumber(stats.completedExecutions), + description: `${stats.successRate}% success rate`, + icon: CheckCircle2, + }, + { + title: "Failed (24h)", + value: stats.failedExecutions24h, + trend: stats.failureChange, + description: "from yesterday", + icon: CircleAlert, + }, + { + title: "Total Executions", + value: formatNumber(stats.totalExecutions), + description: "All time executions", + icon: Clock3, + }, + ]; + + return ( +
+
+
+
+

+ {greeting} 👋 +

+ +

+ Here's what's happening with your workflows. +

+
+
+ +
+ {statCards.map((card) => ( + + ))} +
+ +
+
+ + +
+ +
+ + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/components/dashboard/LiveActivity.tsx b/frontend/components/dashboard/LiveActivity.tsx index 32eefe4..0a21abb 100644 --- a/frontend/components/dashboard/LiveActivity.tsx +++ b/frontend/components/dashboard/LiveActivity.tsx @@ -11,6 +11,7 @@ import { formatRelativeTime } from "@/lib/utils/format-relative-time"; interface LiveActivityProps { activities: Activity[]; + className?: string; } const activityConfig: Record< @@ -18,39 +19,52 @@ const activityConfig: Record< { icon: typeof ActivityIcon; label: string; + iconClassName: string; + bgClassName: string; } > = { APPROVAL_REQUESTED: { icon: Clock3, label: "Approval Requested", + iconClassName: "text-amber-400", + bgClassName: "bg-amber-500/10 border border-amber-500/20", }, EXECUTION_COMPLETED: { icon: CheckCircle2, label: "Execution Completed", + iconClassName: "text-emerald-400", + bgClassName: "bg-emerald-500/10 border border-emerald-500/20", }, EXECUTION_STARTED: { icon: Play, label: "Execution Started", + iconClassName: "text-sky-400 fill-sky-400/20", + bgClassName: "bg-sky-500/10 border border-sky-500/20", }, EXECUTION_FAILED: { icon: CircleAlert, label: "Execution Failed", + iconClassName: "text-red-400", + bgClassName: "bg-red-500/10 border border-red-500/20", }, }; export default function LiveActivity({ activities, + className = "", }: LiveActivityProps) { return ( -
-
+
+

Live Activity

Latest activity across your workflows

-
+
{activities.map((activity) => { const config = activityConfig[activity.type]; const Icon = config.icon; @@ -58,10 +72,10 @@ export default function LiveActivity({ return (
-
- +
+
@@ -69,7 +83,7 @@ export default function LiveActivity({ {config.label}

-

+

{activity.message}

@@ -90,3 +104,4 @@ export default function LiveActivity({
); } + diff --git a/frontend/components/dashboard/QuickActions.tsx b/frontend/components/dashboard/QuickActions.tsx index 8cc963e..abb9a9b 100644 --- a/frontend/components/dashboard/QuickActions.tsx +++ b/frontend/components/dashboard/QuickActions.tsx @@ -1,80 +1,97 @@ -"use client"; - -import { useRouter } from "next/navigation"; -import { - Plus, - Play, - LayoutTemplate, - History, -} from "lucide-react"; - -export default function QuickActions() { - const router = useRouter(); - - const actions = [ - { - label: "Create Workflow", - description: "Build a new automation", - icon: Plus, - onClick: () => router.push("/workflows/new"), - }, - { - label: "Run Workflow", - description: "Execute an existing workflow", - icon: Play, - onClick: () => router.push("/workflows"), - }, - { - label: "Browse Templates", - description: "Start from a template", - icon: LayoutTemplate, - onClick: () => router.push("/templates"), - }, - { - label: "View History", - description: "Check past executions", - icon: History, - onClick: () => router.push("/executions"), - }, - ]; - - return ( -
-
-

Quick Actions

-

- Common workflow actions -

-
- -
- {actions.map((action) => { - const Icon = action.icon; - - return ( - - ); - })} -
-
- ); -} \ No newline at end of file +"use client"; + +import { useRouter } from "next/navigation"; +import { + Plus, + Play, + LayoutTemplate, + History, + ChevronRight, +} from "lucide-react"; + +export default function QuickActions() { + const router = useRouter(); + + const actions = [ + { + label: "Create Workflow", + description: "Build a new automation", + icon: Plus, + isPrimary: true, + iconBg: "bg-[#8174ff]/15 text-[#a49aff] border border-[#8174ff]/30", + onClick: () => router.push("/workflows/new"), + }, + { + label: "Run Workflow", + description: "Execute an existing workflow", + icon: Play, + isPrimary: false, + iconBg: "bg-blue-500/10 text-blue-400 border border-blue-500/20", + onClick: () => router.push("/workflows"), + }, + { + label: "Browse Templates", + description: "Start from a template", + icon: LayoutTemplate, + isPrimary: false, + iconBg: "bg-violet-500/10 text-violet-400 border border-violet-500/20", + onClick: () => router.push("/templates"), + }, + { + label: "View History", + description: "Check past executions", + icon: History, + isPrimary: false, + iconBg: "bg-amber-500/10 text-amber-400 border border-amber-500/20", + onClick: () => router.push("/executions"), + }, + ]; + + return ( +
+
+

Quick Actions

+

+ Common workflow actions +

+
+ +
+ {actions.map((action) => { + const Icon = action.icon; + + return ( + + ); + })} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/components/dashboard/RunningExecutions.tsx b/frontend/components/dashboard/RunningExecutions.tsx index 996b4fd..d60ab13 100644 --- a/frontend/components/dashboard/RunningExecutions.tsx +++ b/frontend/components/dashboard/RunningExecutions.tsx @@ -30,7 +30,7 @@ export default function RunningExecutions({
{executions.map((execution) => { - const status = statusConfig[execution.status]; + const status = statusConfig[execution.status] || statusConfig.RUNNING; return (
0 + ? "text-red-400 bg-red-500/10 border-red-500/20" + : "text-zinc-400 bg-zinc-800/50 border-zinc-700/30", }, { label: "Active Instances", value: health.activeInstances, + badgeClass: "text-sky-400 bg-sky-500/10 border-sky-500/20", }, { label: "Avg. Execution Time", value: `${health.averageExecutionTime}s`, - }, - { - label: "Resource Usage", - value: health.resourceUsage, + badgeClass: "text-zinc-300 bg-zinc-800/40 border-zinc-700/30", }, ]; @@ -48,13 +47,15 @@ export default function SystemHealth({ {healthItems.map((item) => (
{item.label} - + {item.value}
@@ -62,4 +63,4 @@ export default function SystemHealth({
); -} \ No newline at end of file +} diff --git a/frontend/components/execution-details/ActiveLogs.tsx b/frontend/components/execution-details/ActiveLogs.tsx index 3df8da9..1b35ca3 100644 --- a/frontend/components/execution-details/ActiveLogs.tsx +++ b/frontend/components/execution-details/ActiveLogs.tsx @@ -1,15 +1,22 @@ +"use client"; + +import type { + ExecutionDetails, + ExecutionLog, +} from "@/types/execution-details"; + const logLevelConfig = { INFO: { - className: "text-blue-500", + className: "bg-blue-500/10 text-blue-400 border-blue-500/20", }, WAIT: { - className: "text-amber-500", + className: "bg-amber-500/10 text-amber-400 border-amber-500/20", }, SYSTEM: { - className: "text-muted-foreground", + className: "bg-zinc-800/60 text-zinc-400 border-zinc-700/40", }, ERROR: { - className: "text-red-500", + className: "bg-red-500/10 text-red-400 border-red-500/20", }, } as const; @@ -17,63 +24,55 @@ interface LogRowProps { log: ExecutionLog; } -function LogRow({ - log, -}: LogRowProps) { +function LogRow({ log }: LogRowProps) { const config = - logLevelConfig[log.level]; + logLevelConfig[log.level] || logLevelConfig.SYSTEM; return ( -
- - +
+ {log.timestamp} - - {log.level} - - - {log.message} +
+ + {log.level} + +
+ + {log.message} +
); } -import type { - ExecutionDetails, - ExecutionLog, -} from "@/types/execution-details"; - interface ActiveLogsProps { execution: ExecutionDetails; } -export default function ActiveLogs({ - execution, -}: ActiveLogsProps) { +export default function ActiveLogs({ execution }: ActiveLogsProps) { return ( -
- -
- -

- Active Logs +
+
+

+ Execution Logs

-
-
- - {execution.logs.map((log) => ( - - ))} - +
+ {execution.logs.length === 0 ? ( +

+ No execution logs available. +

+ ) : ( + execution.logs.map((log) => ( + + )) + )}
-
); } \ No newline at end of file diff --git a/frontend/components/execution-details/ExecutionDetailsView.tsx b/frontend/components/execution-details/ExecutionDetailsView.tsx index 95c765e..b6b9dfe 100644 --- a/frontend/components/execution-details/ExecutionDetailsView.tsx +++ b/frontend/components/execution-details/ExecutionDetailsView.tsx @@ -1,66 +1,151 @@ "use client"; +import { useEffect, useState, useCallback } from "react"; +import { notFound } from "next/navigation"; +import { AlertCircle, RefreshCw } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Node, Edge } from "@xyflow/react"; + import { ExecutionDetails } from "@/types/execution-details"; +import { getExecutionDetails } from "@/lib/api/execution-details"; +import { getWorkflowDefinition } from "@/lib/api/workflow"; +import { fromWorkflowDefinition } from "@/lib/mappers/workflow.mapper"; import ExecutionHeader from "./ExecutionHeader"; -import ExecutionSidebar from "./ExecutionSidebar"; import WorkflowExecutionCanvas from "./WorkflowExecutionCanvas"; -import { executionWorkflowEdges, executionWorkflowNodes } from "@/mocks/execution-workflow"; import ExecutionTimeline from "./ExecutionTimeline"; import ActiveLogs from "./ActiveLogs"; interface ExecutionDetailsViewProps { - execution: ExecutionDetails; + executionId?: string; + execution?: ExecutionDetails; } export default function ExecutionDetailsView({ - execution, + executionId, + execution: initialExecution, }: ExecutionDetailsViewProps) { + const [execution, setExecution] = useState( + initialExecution || null + ); + const [isLoading, setIsLoading] = useState( + !initialExecution && Boolean(executionId) + ); + const [error, setError] = useState(null); - const nodes = executionWorkflowNodes.map((node) => { - const executionNode = execution.nodes.find( - (n) => n.id === node.id + const [workflowNodes, setWorkflowNodes] = useState([]); + const [workflowEdges, setWorkflowEdges] = useState([]); + + const fetchExecution = useCallback(async () => { + if (!executionId) return; + setIsLoading(true); + setError(null); + try { + const data = await getExecutionDetails(executionId); + if (!data) { + notFound(); + return; + } + setExecution(data); + } catch (err: any) { + console.error("Failed to fetch execution details:", err); + if (err?.response?.status === 404) { + notFound(); + return; + } + setError( + err?.response?.data?.message || + err?.message || + "Failed to load execution details." ); + } finally { + setIsLoading(false); + } + }, [executionId]); + + useEffect(() => { + if (!initialExecution && executionId) { + fetchExecution(); + } + }, [fetchExecution, initialExecution, executionId]); + + useEffect(() => { + async function loadWorkflowCanvas() { + if (!execution?.workflowId) return; + try { + const definition = await getWorkflowDefinition(execution.workflowId); + if (definition) { + const { nodes, edges } = fromWorkflowDefinition(definition); + setWorkflowNodes(nodes); + setWorkflowEdges(edges); + } + } catch (err) { + console.warn("Failed to load workflow canvas definition:", err); + } + } + loadWorkflowCanvas(); + }, [execution?.workflowId]); - return { - ...node, - data: { - ...node.data, - executionStatus: executionNode?.status, - }, - }; - }); + if (isLoading) { + return ( +
+ +

Loading execution details...

+
+ ); + } + + if (error || !execution) { + return ( +
+ +
+

Error Loading Execution

+

{error || "Execution details could not be loaded."}

+
+ +
+ ); + } + + const canvasNodes = workflowNodes.map((node) => { + const executionNode = execution.nodes.find( + (n) => n.id === node.id + ); + + return { + ...node, + data: { + ...node.data, + executionStatus: executionNode?.status || "PENDING", + }, + }; + }); return (
- {/* Left Sidebar */} - - {/* Center */}
{/* Header */} - + {/* Canvas */}
- +
{/* Logs */} - +
{/* Right Timeline */} - +
); } \ No newline at end of file diff --git a/frontend/components/execution-details/ExecutionHeader.tsx b/frontend/components/execution-details/ExecutionHeader.tsx index eb88d2b..d728414 100644 --- a/frontend/components/execution-details/ExecutionHeader.tsx +++ b/frontend/components/execution-details/ExecutionHeader.tsx @@ -1,76 +1,54 @@ "use client"; -import { - Clock3, - Pause, - X, -} from "lucide-react"; - -import { Button } from "@/components/ui/button"; - +import { Clock3 } from "lucide-react"; import { ExecutionDetails } from "@/types/execution-details"; interface ExecutionHeaderProps { execution: ExecutionDetails; } +const statusStyles: Record = { + COMPLETED: "bg-emerald-500/10 text-emerald-500 border-emerald-500/20", + RUNNING: "bg-blue-500/10 text-blue-500 border-blue-500/20", + FAILED: "bg-destructive/10 text-destructive border-destructive/20", + WAITING: "bg-amber-500/10 text-amber-500 border-amber-500/20", +}; + export default function ExecutionHeader({ execution, }: ExecutionHeaderProps) { + const statusClass = + statusStyles[execution.status?.toUpperCase()] || + "bg-muted text-muted-foreground border-border"; + return (
- {/* Left */}
-
ID: {execution.id}
+
+ {execution.status} +
{/* Right */} -
-
- - - {execution.duration} - + {execution.duration}
- - - - -
-
); } \ No newline at end of file diff --git a/frontend/components/execution-details/ExecutionTimeline.tsx b/frontend/components/execution-details/ExecutionTimeline.tsx index 9c07bb7..802d333 100644 --- a/frontend/components/execution-details/ExecutionTimeline.tsx +++ b/frontend/components/execution-details/ExecutionTimeline.tsx @@ -1,10 +1,19 @@ -import type { ExecutionTimelineItem } from "@/types/execution-details"; +"use client"; + +import { useState } from "react"; +import type { ExecutionTimelineItem, ExecutionDetails } from "@/types/execution-details"; import { cn } from "@/lib/utils"; const timelineStatusConfig = { SUCCESS: { dot: "bg-green-500", }, + RUNNING: { + dot: "bg-blue-500", + }, + PENDING: { + dot: "bg-zinc-500", + }, WAITING: { dot: "bg-amber-500", }, @@ -18,57 +27,56 @@ interface TimelineItemProps { isLast: boolean; } -function TimelineItem({ - item, - isLast, -}: TimelineItemProps) { +function TimelineItem({ item, isLast }: TimelineItemProps) { + const [showDetails, setShowDetails] = useState(false); const config = - timelineStatusConfig[item.status]; + timelineStatusConfig[item.status] || timelineStatusConfig.SUCCESS; return (
-
+
-
- - {!isLast && ( -
- )} - + {!isLast &&
}
-
- -
- -

- {item.title} -

+
+
+

{item.title}

- + {item.timestamp} -
-

- {item.description} -

+ {item.description && ( +

+ {item.description} +

+ )} + {item.status === "FAILED" && item.errorMessage && ( +
+ + + {showDetails && ( +
+                {item.errorMessage}
+              
+ )} +
+ )}
-
); } -import { ExecutionDetails } from "@/types/execution-details"; - interface ExecutionTimelineProps { execution: ExecutionDetails; } @@ -77,25 +85,16 @@ export default function ExecutionTimeline({ execution, }: ExecutionTimelineProps) { return ( -