@roj-ai/debug 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/debug/DebugContext.d.ts +10 -0
- package/dist/components/debug/DebugNavigation.d.ts +29 -0
- package/dist/components/debug/DebugShell.d.ts +18 -0
- package/dist/components/debug/LLMCallDetail.d.ts +7 -0
- package/dist/components/debug/TimelineDetailInspector.d.ts +6 -0
- package/dist/components/debug/communication/CommunicationDiagram.d.ts +9 -0
- package/dist/components/debug/communication/DiagramHeader.d.ts +7 -0
- package/dist/components/debug/communication/ParticipantLane.d.ts +7 -0
- package/dist/components/debug/communication/TimeAxis.d.ts +9 -0
- package/dist/components/debug/communication/elements/IdleGap.d.ts +9 -0
- package/dist/components/debug/communication/elements/LLMBlock.d.ts +9 -0
- package/dist/components/debug/communication/elements/MessageArrow.d.ts +10 -0
- package/dist/components/debug/communication/elements/ToolBlock.d.ts +9 -0
- package/dist/components/debug/communication/hooks/useDiagramData.d.ts +12 -0
- package/dist/components/debug/communication/hooks/useTimeCompression.d.ts +7 -0
- package/dist/components/debug/communication/hooks/useZoomPan.d.ts +11 -0
- package/dist/components/debug/communication/popovers/ElementPopover.d.ts +8 -0
- package/dist/components/debug/communication/types.d.ts +136 -0
- package/dist/components/debug/index.d.ts +11 -0
- package/dist/components/debug/pages/AgentDetailPage.d.ts +3 -0
- package/dist/components/debug/pages/AgentsPage.d.ts +1 -0
- package/dist/components/debug/pages/CommunicationPage.d.ts +1 -0
- package/dist/components/debug/pages/DashboardPage.d.ts +1 -0
- package/dist/components/debug/pages/EventsPage.d.ts +1 -0
- package/dist/components/debug/pages/FilesPage.d.ts +1 -0
- package/dist/components/debug/pages/LLMCallPage.d.ts +1 -0
- package/dist/components/debug/pages/LLMCallsPage.d.ts +1 -0
- package/dist/components/debug/pages/LogsPage.d.ts +1 -0
- package/dist/components/debug/pages/MailboxPage.d.ts +1 -0
- package/dist/components/debug/pages/ServicesPage.d.ts +1 -0
- package/dist/components/debug/pages/TimelinePage.d.ts +1 -0
- package/dist/components/debug/pages/UserChatPage.d.ts +1 -0
- package/dist/components/debug/pages/index.d.ts +13 -0
- package/dist/index.d.ts +9 -0
- package/dist/lib/domain-utils.d.ts +7 -0
- package/dist/providers/EventPollingProvider.d.ts +27 -0
- package/dist/stores/event-store.d.ts +93 -0
- package/dist/utils/format.d.ts +1 -0
- package/package.json +43 -0
- package/src/components/debug/DebugContext.tsx +18 -0
- package/src/components/debug/DebugNavigation.tsx +55 -0
- package/src/components/debug/DebugShell.tsx +321 -0
- package/src/components/debug/LLMCallDetail.tsx +740 -0
- package/src/components/debug/TimelineDetailInspector.tsx +204 -0
- package/src/components/debug/communication/CommunicationDiagram.tsx +260 -0
- package/src/components/debug/communication/DiagramHeader.tsx +113 -0
- package/src/components/debug/communication/ParticipantLane.tsx +60 -0
- package/src/components/debug/communication/TimeAxis.tsx +106 -0
- package/src/components/debug/communication/elements/IdleGap.tsx +90 -0
- package/src/components/debug/communication/elements/LLMBlock.tsx +107 -0
- package/src/components/debug/communication/elements/MessageArrow.tsx +119 -0
- package/src/components/debug/communication/elements/ToolBlock.tsx +99 -0
- package/src/components/debug/communication/hooks/useDiagramData.ts +294 -0
- package/src/components/debug/communication/hooks/useTimeCompression.ts +140 -0
- package/src/components/debug/communication/hooks/useZoomPan.ts +87 -0
- package/src/components/debug/communication/popovers/ElementPopover.tsx +158 -0
- package/src/components/debug/communication/types.ts +180 -0
- package/src/components/debug/index.ts +37 -0
- package/src/components/debug/pages/AgentDetailPage.tsx +1295 -0
- package/src/components/debug/pages/AgentsPage.tsx +297 -0
- package/src/components/debug/pages/CommunicationPage.tsx +89 -0
- package/src/components/debug/pages/DashboardPage.tsx +1504 -0
- package/src/components/debug/pages/EventsPage.tsx +276 -0
- package/src/components/debug/pages/FilesPage.tsx +366 -0
- package/src/components/debug/pages/LLMCallPage.tsx +32 -0
- package/src/components/debug/pages/LLMCallsPage.tsx +473 -0
- package/src/components/debug/pages/LogsPage.tsx +199 -0
- package/src/components/debug/pages/MailboxPage.tsx +232 -0
- package/src/components/debug/pages/ServicesPage.tsx +193 -0
- package/src/components/debug/pages/TimelinePage.tsx +569 -0
- package/src/components/debug/pages/UserChatPage.tsx +250 -0
- package/src/components/debug/pages/index.ts +13 -0
- package/src/index.ts +55 -0
- package/src/lib/domain-utils.ts +12 -0
- package/src/providers/EventPollingProvider.tsx +60 -0
- package/src/stores/event-store.ts +497 -0
- package/src/utils/format.ts +8 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import type { GlobalMailboxMessage } from '@roj-ai/shared'
|
|
2
|
+
import { useState } from 'react'
|
|
3
|
+
import { useEventStore, useGlobalMailbox } from '../../../stores/event-store'
|
|
4
|
+
import { DebugLink } from '../DebugNavigation'
|
|
5
|
+
|
|
6
|
+
export function MailboxPage() {
|
|
7
|
+
// Get mailbox from event store (already loaded by DebugLayout)
|
|
8
|
+
const messages = useGlobalMailbox()
|
|
9
|
+
const isLoading = useEventStore((s) => s.isLoading)
|
|
10
|
+
const error = useEventStore((s) => s.error)
|
|
11
|
+
|
|
12
|
+
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set())
|
|
13
|
+
|
|
14
|
+
const toggleExpanded = (id: string) => {
|
|
15
|
+
setExpandedIds((prev) => {
|
|
16
|
+
const next = new Set(prev)
|
|
17
|
+
if (next.has(id)) {
|
|
18
|
+
next.delete(id)
|
|
19
|
+
} else {
|
|
20
|
+
next.add(id)
|
|
21
|
+
}
|
|
22
|
+
return next
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Calculate stats
|
|
27
|
+
const total = messages.length
|
|
28
|
+
const pendingCount = messages.filter((m) => !m.consumed).length
|
|
29
|
+
const consumedCount = messages.filter((m) => m.consumed).length
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="space-y-4">
|
|
33
|
+
{/* Summary */}
|
|
34
|
+
<div className="flex items-center gap-6 text-sm">
|
|
35
|
+
<span className="text-slate-600">
|
|
36
|
+
<span className="font-medium text-slate-900">{total}</span> messages
|
|
37
|
+
</span>
|
|
38
|
+
<span className="text-slate-600">
|
|
39
|
+
<span className="font-medium text-green-600">{consumedCount}</span> read
|
|
40
|
+
</span>
|
|
41
|
+
{pendingCount > 0 && (
|
|
42
|
+
<span className="text-slate-600">
|
|
43
|
+
<span className="font-medium text-blue-600">{pendingCount}</span> pending
|
|
44
|
+
</span>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
{/* Error */}
|
|
49
|
+
{error && <div className="text-red-500 text-sm">{error}</div>}
|
|
50
|
+
|
|
51
|
+
{/* Loading */}
|
|
52
|
+
{isLoading && messages.length === 0 && <div className="text-slate-500 text-sm">Loading mailbox...</div>}
|
|
53
|
+
|
|
54
|
+
{/* Table */}
|
|
55
|
+
{messages.length > 0 && (
|
|
56
|
+
<div className="bg-white rounded-md border border-slate-200 overflow-hidden">
|
|
57
|
+
<div className="overflow-x-auto">
|
|
58
|
+
<table className="w-full text-sm">
|
|
59
|
+
<thead className="bg-slate-50 border-b border-slate-200">
|
|
60
|
+
<tr>
|
|
61
|
+
<th className="px-3 py-2 text-left font-medium text-slate-600">From</th>
|
|
62
|
+
<th className="px-3 py-2 text-center font-medium text-slate-600 w-8"></th>
|
|
63
|
+
<th className="px-3 py-2 text-left font-medium text-slate-600">To</th>
|
|
64
|
+
<th className="px-3 py-2 text-left font-medium text-slate-600">Message</th>
|
|
65
|
+
<th className="px-3 py-2 text-left font-medium text-slate-600">Time</th>
|
|
66
|
+
<th className="px-3 py-2 text-center font-medium text-slate-600">Status</th>
|
|
67
|
+
</tr>
|
|
68
|
+
</thead>
|
|
69
|
+
<tbody className="divide-y divide-slate-200">
|
|
70
|
+
{messages.map((msg) => (
|
|
71
|
+
<MessageRow
|
|
72
|
+
key={msg.id}
|
|
73
|
+
message={msg}
|
|
74
|
+
isExpanded={expandedIds.has(msg.id)}
|
|
75
|
+
onToggleExpand={() => toggleExpanded(msg.id)}
|
|
76
|
+
/>
|
|
77
|
+
))}
|
|
78
|
+
</tbody>
|
|
79
|
+
</table>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
|
|
84
|
+
{/* Empty state */}
|
|
85
|
+
{!isLoading && messages.length === 0 && <div className="text-slate-500 text-sm">No messages found</div>}
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function MessageRow({
|
|
91
|
+
message,
|
|
92
|
+
isExpanded,
|
|
93
|
+
onToggleExpand,
|
|
94
|
+
}: {
|
|
95
|
+
message: GlobalMailboxMessage
|
|
96
|
+
isExpanded: boolean
|
|
97
|
+
onToggleExpand: () => void
|
|
98
|
+
}) {
|
|
99
|
+
const isLongMessage = message.content.length > 100
|
|
100
|
+
const displayContent = isExpanded
|
|
101
|
+
? message.content
|
|
102
|
+
: message.content.slice(0, 100) + (isLongMessage ? '...' : '')
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<tr className="hover:bg-slate-50">
|
|
106
|
+
{/* From */}
|
|
107
|
+
<td className="px-3 py-2">
|
|
108
|
+
<AgentBadge
|
|
109
|
+
agentId={message.fromAgentId}
|
|
110
|
+
agentName={message.fromAgentName}
|
|
111
|
+
|
|
112
|
+
/>
|
|
113
|
+
</td>
|
|
114
|
+
|
|
115
|
+
{/* Arrow */}
|
|
116
|
+
<td className="px-3 py-2 text-center text-slate-400">
|
|
117
|
+
<ArrowIcon />
|
|
118
|
+
</td>
|
|
119
|
+
|
|
120
|
+
{/* To */}
|
|
121
|
+
<td className="px-3 py-2">
|
|
122
|
+
<AgentBadge
|
|
123
|
+
agentId={message.toAgentId}
|
|
124
|
+
agentName={message.toAgentName}
|
|
125
|
+
|
|
126
|
+
/>
|
|
127
|
+
</td>
|
|
128
|
+
|
|
129
|
+
{/* Message */}
|
|
130
|
+
<td className="px-3 py-2">
|
|
131
|
+
<div
|
|
132
|
+
className={`text-slate-700 ${isLongMessage ? 'cursor-pointer' : ''}`}
|
|
133
|
+
onClick={isLongMessage ? onToggleExpand : undefined}
|
|
134
|
+
>
|
|
135
|
+
<span className="whitespace-pre-wrap break-words">{displayContent}</span>
|
|
136
|
+
{isLongMessage && (
|
|
137
|
+
<button
|
|
138
|
+
onClick={(e) => {
|
|
139
|
+
e.stopPropagation()
|
|
140
|
+
onToggleExpand()
|
|
141
|
+
}}
|
|
142
|
+
className="ml-2 text-violet-600 hover:text-violet-800 text-xs"
|
|
143
|
+
>
|
|
144
|
+
{isExpanded ? 'less' : 'more'}
|
|
145
|
+
</button>
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
148
|
+
</td>
|
|
149
|
+
|
|
150
|
+
{/* Time */}
|
|
151
|
+
<td className="px-3 py-2 font-mono text-xs text-slate-500 whitespace-nowrap">
|
|
152
|
+
{new Date(message.timestamp).toLocaleTimeString()}
|
|
153
|
+
</td>
|
|
154
|
+
|
|
155
|
+
{/* Status */}
|
|
156
|
+
<td className="px-3 py-2 text-center">
|
|
157
|
+
<StatusBadge consumed={message.consumed} />
|
|
158
|
+
</td>
|
|
159
|
+
</tr>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function AgentBadge({
|
|
164
|
+
agentId,
|
|
165
|
+
agentName,
|
|
166
|
+
}: {
|
|
167
|
+
agentId: string
|
|
168
|
+
agentName: string
|
|
169
|
+
}) {
|
|
170
|
+
const isSpecial = agentId === 'user' || agentId === 'orchestrator' || agentId === 'communicator'
|
|
171
|
+
|
|
172
|
+
if (isSpecial) {
|
|
173
|
+
return (
|
|
174
|
+
<span
|
|
175
|
+
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium ${
|
|
176
|
+
agentId === 'user'
|
|
177
|
+
? 'bg-blue-100 text-blue-700'
|
|
178
|
+
: 'bg-purple-100 text-purple-700'
|
|
179
|
+
}`}
|
|
180
|
+
>
|
|
181
|
+
<span className="truncate max-w-24" title={agentName}>
|
|
182
|
+
{agentName}
|
|
183
|
+
</span>
|
|
184
|
+
</span>
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<DebugLink
|
|
190
|
+
to={`agents/${agentId}`}
|
|
191
|
+
className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium bg-slate-100 text-slate-700 hover:bg-slate-200"
|
|
192
|
+
>
|
|
193
|
+
<span className="truncate max-w-24" title={agentName}>
|
|
194
|
+
{agentName}
|
|
195
|
+
</span>
|
|
196
|
+
</DebugLink>
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function StatusBadge({ consumed }: { consumed: boolean }) {
|
|
201
|
+
if (consumed) {
|
|
202
|
+
return (
|
|
203
|
+
<span className="inline-flex items-center gap-1 text-xs text-slate-500">
|
|
204
|
+
<CheckIcon />
|
|
205
|
+
<span>Read</span>
|
|
206
|
+
</span>
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<span className="inline-flex items-center gap-1 text-xs text-blue-600">
|
|
212
|
+
<span className="w-2 h-2 rounded-full bg-blue-500 animate-pulse" />
|
|
213
|
+
<span>Pending</span>
|
|
214
|
+
</span>
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function ArrowIcon() {
|
|
219
|
+
return (
|
|
220
|
+
<svg className="w-4 h-4 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
221
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
|
222
|
+
</svg>
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function CheckIcon() {
|
|
227
|
+
return (
|
|
228
|
+
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
229
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
230
|
+
</svg>
|
|
231
|
+
)
|
|
232
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react'
|
|
2
|
+
import type { ServiceEntry, ServiceStatus } from '@roj-ai/shared'
|
|
3
|
+
import { api } from '@roj-ai/client'
|
|
4
|
+
import { useDebugSessionId } from '../DebugNavigation'
|
|
5
|
+
import { useEventStore } from '../../../stores/event-store'
|
|
6
|
+
|
|
7
|
+
const statusColors: Record<ServiceStatus, string> = {
|
|
8
|
+
stopped: 'bg-slate-100 text-slate-700',
|
|
9
|
+
starting: 'bg-yellow-100 text-yellow-700',
|
|
10
|
+
ready: 'bg-green-100 text-green-700',
|
|
11
|
+
stopping: 'bg-orange-100 text-orange-700',
|
|
12
|
+
failed: 'bg-red-100 text-red-700',
|
|
13
|
+
paused: 'bg-blue-100 text-blue-700',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function formatTimestamp(ts: number | undefined): string {
|
|
17
|
+
if (ts === undefined) return '-'
|
|
18
|
+
return new Date(ts).toLocaleTimeString()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function ServicesPage() {
|
|
22
|
+
const sessionId = useDebugSessionId()
|
|
23
|
+
const services = useEventStore((s) => s.servicesProjectionState.services)
|
|
24
|
+
|
|
25
|
+
if (!sessionId) return null
|
|
26
|
+
|
|
27
|
+
if (services.size === 0) {
|
|
28
|
+
return (
|
|
29
|
+
<div className="bg-white rounded-md border border-slate-200 p-8 text-center text-slate-500 text-sm">
|
|
30
|
+
No services configured
|
|
31
|
+
</div>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="flex flex-col gap-4">
|
|
37
|
+
<div className="bg-white rounded-md border border-slate-200 overflow-hidden">
|
|
38
|
+
<table className="w-full text-sm">
|
|
39
|
+
<thead>
|
|
40
|
+
<tr className="border-b border-slate-200 bg-slate-50 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
|
|
41
|
+
<th className="px-4 py-3">Service</th>
|
|
42
|
+
<th className="px-4 py-3">Status</th>
|
|
43
|
+
<th className="px-4 py-3">Port</th>
|
|
44
|
+
<th className="px-4 py-3">Started</th>
|
|
45
|
+
<th className="px-4 py-3">Ready</th>
|
|
46
|
+
<th className="px-4 py-3">Stopped</th>
|
|
47
|
+
<th className="px-4 py-3">Error</th>
|
|
48
|
+
<th className="px-4 py-3">Actions</th>
|
|
49
|
+
</tr>
|
|
50
|
+
</thead>
|
|
51
|
+
<tbody className="divide-y divide-slate-100">
|
|
52
|
+
{[...services.entries()].map(([key, entry]) => (
|
|
53
|
+
<ServiceRow key={key} entry={entry} sessionId={sessionId} />
|
|
54
|
+
))}
|
|
55
|
+
</tbody>
|
|
56
|
+
</table>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function ServiceRow({ entry, sessionId }: { entry: ServiceEntry; sessionId: string }) {
|
|
63
|
+
const [loading, setLoading] = useState<string | null>(null)
|
|
64
|
+
const [error, setError] = useState<string | null>(null)
|
|
65
|
+
const [logs, setLogs] = useState<string[] | null>(null)
|
|
66
|
+
|
|
67
|
+
const callAction = useCallback(async (action: 'start' | 'stop' | 'restart') => {
|
|
68
|
+
setLoading(action)
|
|
69
|
+
setError(null)
|
|
70
|
+
try {
|
|
71
|
+
const result = action === 'start'
|
|
72
|
+
? await api.call('services.start', { sessionId, serviceType: entry.serviceType })
|
|
73
|
+
: action === 'stop'
|
|
74
|
+
? await api.call('services.stop', { sessionId, serviceType: entry.serviceType })
|
|
75
|
+
: await api.call('services.restart', { sessionId, serviceType: entry.serviceType })
|
|
76
|
+
|
|
77
|
+
if (!result.ok) {
|
|
78
|
+
setError(result.error.message)
|
|
79
|
+
}
|
|
80
|
+
} catch (e) {
|
|
81
|
+
setError(e instanceof Error ? e.message : 'Unknown error')
|
|
82
|
+
} finally {
|
|
83
|
+
setLoading(null)
|
|
84
|
+
}
|
|
85
|
+
}, [sessionId, entry.serviceType])
|
|
86
|
+
|
|
87
|
+
const fetchLogs = useCallback(async () => {
|
|
88
|
+
if (logs !== null) {
|
|
89
|
+
setLogs(null)
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
setLoading('logs')
|
|
93
|
+
setError(null)
|
|
94
|
+
try {
|
|
95
|
+
const result = await api.call('services.logs', { sessionId, serviceType: entry.serviceType, lines: 100 })
|
|
96
|
+
if (result.ok) {
|
|
97
|
+
setLogs(result.value.lines)
|
|
98
|
+
} else {
|
|
99
|
+
setError(result.error.message)
|
|
100
|
+
}
|
|
101
|
+
} catch (e) {
|
|
102
|
+
setError(e instanceof Error ? e.message : 'Unknown error')
|
|
103
|
+
} finally {
|
|
104
|
+
setLoading(null)
|
|
105
|
+
}
|
|
106
|
+
}, [sessionId, entry.serviceType, logs])
|
|
107
|
+
|
|
108
|
+
const isRunning = entry.status === 'ready' || entry.status === 'starting'
|
|
109
|
+
const isStopped = entry.status === 'stopped' || entry.status === 'failed'
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<>
|
|
113
|
+
<tr className="hover:bg-slate-50 transition-colors">
|
|
114
|
+
<td className="px-4 py-3 font-mono font-medium text-slate-900">{entry.serviceType}</td>
|
|
115
|
+
<td className="px-4 py-3">
|
|
116
|
+
<span className={`inline-block text-xs px-2 py-0.5 rounded-full font-medium ${statusColors[entry.status]}`}>
|
|
117
|
+
{entry.status}
|
|
118
|
+
</span>
|
|
119
|
+
</td>
|
|
120
|
+
<td className="px-4 py-3 font-mono text-slate-600">{entry.port ?? '-'}</td>
|
|
121
|
+
<td className="px-4 py-3 text-slate-500">{formatTimestamp(entry.startedAt)}</td>
|
|
122
|
+
<td className="px-4 py-3 text-slate-500">{formatTimestamp(entry.readyAt)}</td>
|
|
123
|
+
<td className="px-4 py-3 text-slate-500">{formatTimestamp(entry.stoppedAt)}</td>
|
|
124
|
+
<td className="px-4 py-3 text-red-600 text-xs">{entry.error ?? '-'}</td>
|
|
125
|
+
<td className="px-4 py-3">
|
|
126
|
+
<div className="flex items-center gap-1.5">
|
|
127
|
+
{isStopped && (
|
|
128
|
+
<ActionButton onClick={() => callAction('start')} loading={loading === 'start'} color="green">
|
|
129
|
+
Start
|
|
130
|
+
</ActionButton>
|
|
131
|
+
)}
|
|
132
|
+
{isRunning && (
|
|
133
|
+
<ActionButton onClick={() => callAction('stop')} loading={loading === 'stop'} color="red">
|
|
134
|
+
Stop
|
|
135
|
+
</ActionButton>
|
|
136
|
+
)}
|
|
137
|
+
{isRunning && (
|
|
138
|
+
<ActionButton onClick={() => callAction('restart')} loading={loading === 'restart'} color="yellow">
|
|
139
|
+
Restart
|
|
140
|
+
</ActionButton>
|
|
141
|
+
)}
|
|
142
|
+
<ActionButton onClick={fetchLogs} loading={loading === 'logs'} color="slate" active={logs !== null}>
|
|
143
|
+
Logs
|
|
144
|
+
</ActionButton>
|
|
145
|
+
</div>
|
|
146
|
+
{error && <div className="mt-1 text-xs text-red-600">{error}</div>}
|
|
147
|
+
</td>
|
|
148
|
+
</tr>
|
|
149
|
+
{logs !== null && (
|
|
150
|
+
<tr>
|
|
151
|
+
<td colSpan={8} className="px-4 py-2 bg-slate-900">
|
|
152
|
+
<div className="max-h-80 overflow-auto font-mono text-xs text-slate-300 whitespace-pre-wrap">
|
|
153
|
+
{logs.length === 0
|
|
154
|
+
? <span className="text-slate-500">No logs available</span>
|
|
155
|
+
: logs.join('\n')}
|
|
156
|
+
</div>
|
|
157
|
+
</td>
|
|
158
|
+
</tr>
|
|
159
|
+
)}
|
|
160
|
+
</>
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const buttonColors = {
|
|
165
|
+
green: 'bg-green-50 text-green-700 hover:bg-green-100 border-green-200',
|
|
166
|
+
red: 'bg-red-50 text-red-700 hover:bg-red-100 border-red-200',
|
|
167
|
+
yellow: 'bg-yellow-50 text-yellow-700 hover:bg-yellow-100 border-yellow-200',
|
|
168
|
+
slate: 'bg-slate-50 text-slate-700 hover:bg-slate-100 border-slate-200',
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function ActionButton({
|
|
172
|
+
onClick,
|
|
173
|
+
loading,
|
|
174
|
+
color,
|
|
175
|
+
active,
|
|
176
|
+
children,
|
|
177
|
+
}: {
|
|
178
|
+
onClick: () => void
|
|
179
|
+
loading: boolean
|
|
180
|
+
color: keyof typeof buttonColors
|
|
181
|
+
active?: boolean
|
|
182
|
+
children: React.ReactNode
|
|
183
|
+
}) {
|
|
184
|
+
return (
|
|
185
|
+
<button
|
|
186
|
+
onClick={onClick}
|
|
187
|
+
disabled={loading}
|
|
188
|
+
className={`px-2 py-1 text-xs font-medium rounded border transition-colors disabled:opacity-50 ${active ? 'ring-1 ring-offset-1 ring-slate-400' : ''} ${buttonColors[color]}`}
|
|
189
|
+
>
|
|
190
|
+
{loading ? '...' : children}
|
|
191
|
+
</button>
|
|
192
|
+
)
|
|
193
|
+
}
|