Skip to content

Commit c635450

Browse files
feat: Add workflow status dashboard page
Add workflows page for monitoring and managing automated workflow executions: Workflows Page Features: - Real-time workflow execution monitoring with 5-second polling - Status cards showing total/running/completed/failed counts - Status filtering (all, running, completed, failed) - Workflow list with expandable details Workflow Details: - Workflow ID and execution ID display - Status badges with icons (pending, running, completed, failed, cancelled) - Associated alert ID linking - Start and completion timestamps - Current step indicator Workflow Steps Visualization: - Sequential step display with status icons - Step type indicators (activity, decision, human_task) - Error messages for failed steps - Step timestamps - Expandable/collapsible details Navigation: - Add Workflows link to sidebar navigation - Add Activity icon for workflows menu item - Configure /workflows route in App.tsx UI Components: - Status color coding (blue=running, green=completed, red=failed) - Filter buttons with active state - Refresh button for manual polling - Expandable workflow detail view API Integration: - Fetch workflows via api.workflows.getExecutions() - Status filter parameter support - Auto-refetch every 5 seconds Related: #phase2-workflow-dashboard
1 parent 3e5a72a commit c635450

3 files changed

Lines changed: 327 additions & 0 deletions

File tree

services/web_dashboard/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { Reports } from '@/pages/Reports'
1515
import { Settings } from '@/pages/Settings'
1616
import { Automation } from '@/pages/Automation'
1717
import { Notifications } from '@/pages/Notifications'
18+
import { Workflows } from '@/pages/Workflows'
1819

1920
// Protected Route Wrapper
2021
const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
@@ -81,6 +82,7 @@ function App() {
8182
<Route path="alerts/:id" element={<AlertDetail />} />
8283
<Route path="reports" element={<Reports />} />
8384
<Route path="automation" element={<Automation />} />
85+
<Route path="workflows" element={<Workflows />} />
8486
<Route path="notifications" element={<Notifications />} />
8587
<Route path="settings" element={<Settings />} />
8688
</Route>

services/web_dashboard/src/components/Layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ import {
1717
X,
1818
Shield,
1919
Zap,
20+
Activity,
2021
} from 'lucide-react'
2122

2223
const navigation = [
2324
{ name: 'Dashboard', href: '/', icon: LayoutDashboard },
2425
{ name: 'Alerts', href: '/alerts', icon: AlertTriangle },
2526
{ name: 'Reports', href: '/reports', icon: FileText },
2627
{ name: 'Automation', href: '/automation', icon: Zap },
28+
{ name: 'Workflows', href: '/workflows', icon: Activity },
2729
{ name: 'Notifications', href: '/notifications', icon: Bell },
2830
{ name: 'Settings', href: '/settings', icon: Settings },
2931
]
Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
/**
2+
* Workflows Page - Display workflow status and executions
3+
*/
4+
5+
import React, { useState } from 'react'
6+
import { useQuery } from '@tanstack/react-query'
7+
import { api } from '@/lib/api'
8+
import {
9+
Play,
10+
Pause,
11+
CheckCircle,
12+
XCircle,
13+
Clock,
14+
AlertTriangle,
15+
Eye,
16+
RefreshCw,
17+
Filter,
18+
} from 'lucide-react'
19+
20+
const statusColors = {
21+
pending: 'bg-gray-100 text-gray-800',
22+
running: 'bg-blue-100 text-blue-800',
23+
completed: 'bg-green-100 text-green-800',
24+
failed: 'bg-red-100 text-red-800',
25+
cancelled: 'bg-yellow-100 text-yellow-800',
26+
}
27+
28+
const statusIcons = {
29+
pending: Clock,
30+
running: RefreshCw,
31+
completed: CheckCircle,
32+
failed: XCircle,
33+
cancelled: XCircle,
34+
}
35+
36+
interface WorkflowExecution {
37+
workflow_id: string
38+
execution_id: string
39+
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'
40+
current_step?: string
41+
started_at: string
42+
completed_at?: string
43+
alert_id?: string
44+
playbook_id?: string
45+
steps: WorkflowStep[]
46+
}
47+
48+
interface WorkflowStep {
49+
step_id: string
50+
name: string
51+
type: 'activity' | 'decision' | 'human_task'
52+
status: 'pending' | 'running' | 'completed' | 'failed' | 'skipped'
53+
started_at?: string
54+
completed_at?: string
55+
error?: string
56+
}
57+
58+
export const Workflows: React.FC = () => {
59+
const [selectedWorkflow, setSelectedWorkflow] = useState<string | null>(null)
60+
const [statusFilter, setStatusFilter] = useState<string>('all')
61+
62+
// Fetch workflow executions
63+
const { data: workflows, isLoading, refetch } = useQuery({
64+
queryKey: ['workflows', statusFilter],
65+
queryFn: () => api.workflows.getExecutions({ status: statusFilter === 'all' ? undefined : statusFilter }),
66+
refetchInterval: 5000, // Poll every 5 seconds
67+
})
68+
69+
const handleStatusFilter = (status: string) => {
70+
setStatusFilter(status)
71+
}
72+
73+
const getFilteredWorkflows = () => {
74+
if (!workflows) return []
75+
if (statusFilter === 'all') return workflows
76+
return workflows.filter((w: WorkflowExecution) => w.status === statusFilter)
77+
}
78+
79+
if (isLoading) {
80+
return (
81+
<div className="flex items-center justify-center h-64">
82+
<div className="spinner"></div>
83+
</div>
84+
)
85+
}
86+
87+
const filteredWorkflows = getFilteredWorkflows()
88+
const stats = {
89+
total: workflows?.length || 0,
90+
running: workflows?.filter((w: WorkflowExecution) => w.status === 'running').length || 0,
91+
completed: workflows?.filter((w: WorkflowExecution) => w.status === 'completed').length || 0,
92+
failed: workflows?.filter((w: WorkflowExecution) => w.status === 'failed').length || 0,
93+
}
94+
95+
return (
96+
<div className="space-y-6">
97+
{/* Header */}
98+
<div className="flex items-center justify-between">
99+
<div>
100+
<h1 className="text-2xl font-bold text-gray-900">Workflows</h1>
101+
<p className="text-sm text-gray-600 mt-1">
102+
Monitor and manage automated workflow executions
103+
</p>
104+
</div>
105+
<button onClick={() => refetch()} className="btn btn-outline flex items-center gap-2">
106+
<RefreshCw className="w-4 h-4" />
107+
Refresh
108+
</button>
109+
</div>
110+
111+
{/* Stats Cards */}
112+
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
113+
<div className="card">
114+
<div className="card-body">
115+
<div className="flex items-center justify-between">
116+
<div>
117+
<p className="text-sm text-gray-600">Total</p>
118+
<p className="text-2xl font-bold text-gray-900">{stats.total}</p>
119+
</div>
120+
<div className="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center">
121+
<Activity className="w-5 h-5 text-gray-600" />
122+
</div>
123+
</div>
124+
</div>
125+
</div>
126+
<div className="card">
127+
<div className="card-body">
128+
<div className="flex items-center justify-between">
129+
<div>
130+
<p className="text-sm text-gray-600">Running</p>
131+
<p className="text-2xl font-bold text-blue-600">{stats.running}</p>
132+
</div>
133+
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
134+
<RefreshCw className="w-5 h-5 text-blue-600" />
135+
</div>
136+
</div>
137+
</div>
138+
</div>
139+
<div className="card">
140+
<div className="card-body">
141+
<div className="flex items-center justify-between">
142+
<div>
143+
<p className="text-sm text-gray-600">Completed</p>
144+
<p className="text-2xl font-bold text-green-600">{stats.completed}</p>
145+
</div>
146+
<div className="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center">
147+
<CheckCircle className="w-5 h-5 text-green-600" />
148+
</div>
149+
</div>
150+
</div>
151+
</div>
152+
<div className="card">
153+
<div className="card-body">
154+
<div className="flex items-center justify-between">
155+
<div>
156+
<p className="text-sm text-gray-600">Failed</p>
157+
<p className="text-2xl font-bold text-red-600">{stats.failed}</p>
158+
</div>
159+
<div className="w-10 h-10 bg-red-100 rounded-full flex items-center justify-center">
160+
<XCircle className="w-5 h-5 text-red-600" />
161+
</div>
162+
</div>
163+
</div>
164+
</div>
165+
</div>
166+
167+
{/* Status Filters */}
168+
<div className="flex items-center gap-2">
169+
<Filter className="w-4 h-4 text-gray-600" />
170+
<button
171+
onClick={() => handleStatusFilter('all')}
172+
className={`px-3 py-1 rounded-full text-sm ${
173+
statusFilter === 'all' ? 'bg-primary-500 text-white' : 'bg-gray-100 text-gray-600'
174+
}`}
175+
>
176+
All
177+
</button>
178+
<button
179+
onClick={() => handleStatusFilter('running')}
180+
className={`px-3 py-1 rounded-full text-sm ${
181+
statusFilter === 'running' ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-600'
182+
}`}
183+
>
184+
Running
185+
</button>
186+
<button
187+
onClick={() => handleStatusFilter('completed')}
188+
className={`px-3 py-1 rounded-full text-sm ${
189+
statusFilter === 'completed' ? 'bg-green-500 text-white' : 'bg-gray-100 text-gray-600'
190+
}`}
191+
>
192+
Completed
193+
</button>
194+
<button
195+
onClick={() => handleStatusFilter('failed')}
196+
className={`px-3 py-1 rounded-full text-sm ${
197+
statusFilter === 'failed' ? 'bg-red-500 text-white' : 'bg-gray-100 text-gray-600'
198+
}`}
199+
>
200+
Failed
201+
</button>
202+
</div>
203+
204+
{/* Workflow List */}
205+
<div className="space-y-4">
206+
{filteredWorkflows.length === 0 ? (
207+
<div className="card">
208+
<div className="card-body text-center py-12">
209+
<p className="text-gray-600">No workflows found</p>
210+
</div>
211+
</div>
212+
) : (
213+
filteredWorkflows.map((workflow: WorkflowExecution) => {
214+
const StatusIcon = statusIcons[workflow.status]
215+
return (
216+
<div key={workflow.execution_id} className="card">
217+
<div className="card-body">
218+
<div className="flex items-start justify-between">
219+
<div className="flex-1">
220+
<div className="flex items-center gap-3 mb-2">
221+
<h3 className="text-lg font-semibold text-gray-900">{workflow.workflow_id}</h3>
222+
<span className={`badge badge-sm ${statusColors[workflow.status]}`}>
223+
<StatusIcon className="w-3 h-3 mr-1 inline" />
224+
{workflow.status}
225+
</span>
226+
{workflow.alert_id && (
227+
<span className="text-sm text-gray-500">
228+
Alert: {workflow.alert_id}
229+
</span>
230+
)}
231+
</div>
232+
<p className="text-sm text-gray-600">
233+
Started: {new Date(workflow.started_at).toLocaleString()}
234+
</p>
235+
{workflow.completed_at && (
236+
<p className="text-sm text-gray-600">
237+
Completed: {new Date(workflow.completed_at).toLocaleString()}
238+
</p>
239+
)}
240+
{workflow.current_step && (
241+
<p className="text-sm text-blue-600 mt-1">
242+
Current: {workflow.current_step}
243+
</p>
244+
)}
245+
</div>
246+
<button
247+
onClick={() => setSelectedWorkflow(
248+
selectedWorkflow === workflow.execution_id ? null : workflow.execution_id
249+
)}
250+
className="btn btn-sm btn-outline"
251+
>
252+
<Eye className="w-4 h-4 mr-1" />
253+
Details
254+
</button>
255+
</div>
256+
257+
{/* Workflow Steps */}
258+
{selectedWorkflow === workflow.execution_id && (
259+
<div className="mt-4 border-t pt-4">
260+
<h4 className="text-sm font-semibold text-gray-700 mb-3">Execution Steps</h4>
261+
<div className="space-y-2">
262+
{workflow.steps.map((step, idx) => {
263+
const StepIcon = statusIcons[step.status] || Clock
264+
return (
265+
<div key={step.step_id} className="flex items-start gap-3 p-3 rounded-lg bg-gray-50">
266+
<div className="flex-shrink-0 mt-0.5">
267+
<StepIcon className={`w-4 h-4 ${
268+
step.status === 'completed' ? 'text-green-600' :
269+
step.status === 'failed' ? 'text-red-600' :
270+
step.status === 'running' ? 'text-blue-600' :
271+
'text-gray-400'
272+
}`} />
273+
</div>
274+
<div className="flex-1 min-w-0">
275+
<div className="flex items-center gap-2">
276+
<span className="text-sm font-medium text-gray-900">
277+
{idx + 1}. {step.name}
278+
</span>
279+
<span className={`text-xs px-2 py-0.5 rounded ${
280+
step.status === 'completed' ? 'bg-green-100 text-green-800' :
281+
step.status === 'failed' ? 'bg-red-100 text-red-800' :
282+
step.status === 'running' ? 'bg-blue-100 text-blue-800' :
283+
'bg-gray-100 text-gray-800'
284+
}`}>
285+
{step.type}
286+
</span>
287+
</div>
288+
{step.error && (
289+
<p className="text-sm text-red-600 mt-1">{step.error}</p>
290+
)}
291+
{step.started_at && (
292+
<p className="text-xs text-gray-500 mt-1">
293+
{new Date(step.started_at).toLocaleString()}
294+
</p>
295+
)}
296+
</div>
297+
</div>
298+
)
299+
})}
300+
</div>
301+
</div>
302+
)}
303+
</div>
304+
</div>
305+
)
306+
})
307+
)}
308+
</div>
309+
</div>
310+
)
311+
}
312+
313+
const Activity = ({ className }: { className?: string }) => (
314+
<svg
315+
className={className}
316+
fill="none"
317+
stroke="currentColor"
318+
viewBox="0 0 24 24"
319+
strokeWidth={2}
320+
>
321+
<path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
322+
</svg>
323+
)

0 commit comments

Comments
 (0)