railscope 0.1.0
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +227 -0
- data/Rakefile +12 -0
- data/app/assets/stylesheets/railscope/application.css +504 -0
- data/app/controllers/railscope/api/entries_controller.rb +103 -0
- data/app/controllers/railscope/application_controller.rb +12 -0
- data/app/controllers/railscope/dashboard_controller.rb +33 -0
- data/app/controllers/railscope/entries_controller.rb +29 -0
- data/app/helpers/railscope/dashboard_helper.rb +157 -0
- data/app/jobs/railscope/application_job.rb +6 -0
- data/app/jobs/railscope/purge_job.rb +15 -0
- data/app/models/railscope/application_record.rb +12 -0
- data/app/models/railscope/entry.rb +51 -0
- data/app/views/layouts/railscope/application.html.erb +14 -0
- data/app/views/railscope/application/index.html.erb +1 -0
- data/app/views/railscope/dashboard/index.html.erb +70 -0
- data/app/views/railscope/entries/show.html.erb +93 -0
- data/client/.gitignore +1 -0
- data/client/index.html +12 -0
- data/client/package-lock.json +2735 -0
- data/client/package.json +28 -0
- data/client/postcss.config.js +6 -0
- data/client/src/App.tsx +60 -0
- data/client/src/api/client.ts +25 -0
- data/client/src/api/entries.ts +36 -0
- data/client/src/components/Layout.tsx +17 -0
- data/client/src/components/PlaceholderPage.tsx +32 -0
- data/client/src/components/Sidebar.tsx +198 -0
- data/client/src/components/ui/Badge.tsx +67 -0
- data/client/src/components/ui/Card.tsx +38 -0
- data/client/src/components/ui/JsonViewer.tsx +80 -0
- data/client/src/components/ui/Pagination.tsx +45 -0
- data/client/src/components/ui/SearchInput.tsx +70 -0
- data/client/src/components/ui/Table.tsx +68 -0
- data/client/src/index.css +28 -0
- data/client/src/lib/hooks.ts +37 -0
- data/client/src/lib/types.ts +61 -0
- data/client/src/lib/utils.ts +38 -0
- data/client/src/main.tsx +13 -0
- data/client/src/screens/cache/Index.tsx +15 -0
- data/client/src/screens/client-requests/Index.tsx +15 -0
- data/client/src/screens/commands/Index.tsx +133 -0
- data/client/src/screens/commands/Show.tsx +395 -0
- data/client/src/screens/dumps/Index.tsx +15 -0
- data/client/src/screens/events/Index.tsx +15 -0
- data/client/src/screens/exceptions/Index.tsx +155 -0
- data/client/src/screens/exceptions/Show.tsx +480 -0
- data/client/src/screens/gates/Index.tsx +15 -0
- data/client/src/screens/jobs/Index.tsx +153 -0
- data/client/src/screens/jobs/Show.tsx +529 -0
- data/client/src/screens/logs/Index.tsx +15 -0
- data/client/src/screens/mail/Index.tsx +15 -0
- data/client/src/screens/models/Index.tsx +15 -0
- data/client/src/screens/notifications/Index.tsx +15 -0
- data/client/src/screens/queries/Index.tsx +159 -0
- data/client/src/screens/queries/Show.tsx +346 -0
- data/client/src/screens/redis/Index.tsx +15 -0
- data/client/src/screens/requests/Index.tsx +123 -0
- data/client/src/screens/requests/Show.tsx +395 -0
- data/client/src/screens/schedule/Index.tsx +15 -0
- data/client/src/screens/views/Index.tsx +141 -0
- data/client/src/screens/views/Show.tsx +337 -0
- data/client/tailwind.config.js +22 -0
- data/client/tsconfig.json +25 -0
- data/client/tsconfig.node.json +10 -0
- data/client/vite.config.ts +37 -0
- data/config/routes.rb +17 -0
- data/db/migrate/20260131023242_create_railscope_entries.rb +41 -0
- data/lib/generators/railscope/install_generator.rb +33 -0
- data/lib/generators/railscope/templates/initializer.rb +34 -0
- data/lib/railscope/context.rb +91 -0
- data/lib/railscope/engine.rb +85 -0
- data/lib/railscope/entry_data.rb +112 -0
- data/lib/railscope/filter.rb +113 -0
- data/lib/railscope/middleware.rb +162 -0
- data/lib/railscope/storage/base.rb +90 -0
- data/lib/railscope/storage/database.rb +83 -0
- data/lib/railscope/storage/redis_storage.rb +314 -0
- data/lib/railscope/subscribers/base_subscriber.rb +52 -0
- data/lib/railscope/subscribers/command_subscriber.rb +237 -0
- data/lib/railscope/subscribers/exception_subscriber.rb +113 -0
- data/lib/railscope/subscribers/job_subscriber.rb +249 -0
- data/lib/railscope/subscribers/query_subscriber.rb +130 -0
- data/lib/railscope/subscribers/request_subscriber.rb +121 -0
- data/lib/railscope/subscribers/view_subscriber.rb +201 -0
- data/lib/railscope/version.rb +5 -0
- data/lib/railscope.rb +145 -0
- data/lib/tasks/railscope_sample.rake +30 -0
- data/public/railscope/assets/app.css +1 -0
- data/public/railscope/assets/app.js +70 -0
- data/public/railscope/assets/index.html +13 -0
- data/sig/railscope.rbs +4 -0
- metadata +157 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { useParams, useNavigate } from 'react-router-dom'
|
|
3
|
+
import { getEntry } from '@/api/entries'
|
|
4
|
+
import { Entry } from '@/lib/types'
|
|
5
|
+
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'
|
|
6
|
+
import { JsonViewer } from '@/components/ui/JsonViewer'
|
|
7
|
+
|
|
8
|
+
type TabType = 'arguments' | 'options'
|
|
9
|
+
|
|
10
|
+
export default function CommandsShow() {
|
|
11
|
+
const { id } = useParams()
|
|
12
|
+
const navigate = useNavigate()
|
|
13
|
+
const [entry, setEntry] = useState<Entry | null>(null)
|
|
14
|
+
const [batch, setBatch] = useState<Entry[]>([])
|
|
15
|
+
const [loading, setLoading] = useState(true)
|
|
16
|
+
const [currentTab, setCurrentTab] = useState<TabType>('arguments')
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
loadEntry()
|
|
20
|
+
}, [id])
|
|
21
|
+
|
|
22
|
+
async function loadEntry() {
|
|
23
|
+
if (!id) return
|
|
24
|
+
setLoading(true)
|
|
25
|
+
try {
|
|
26
|
+
const response = await getEntry(id)
|
|
27
|
+
setEntry(response.data)
|
|
28
|
+
setBatch(response.batch || [])
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error('Failed to load entry:', error)
|
|
31
|
+
} finally {
|
|
32
|
+
setLoading(false)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (loading) {
|
|
37
|
+
return (
|
|
38
|
+
<div className="p-6">
|
|
39
|
+
<Card>
|
|
40
|
+
<CardContent className="py-12 text-center text-dark-muted">
|
|
41
|
+
Fetching...
|
|
42
|
+
</CardContent>
|
|
43
|
+
</Card>
|
|
44
|
+
</div>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!entry) {
|
|
49
|
+
return (
|
|
50
|
+
<div className="p-6">
|
|
51
|
+
<Card>
|
|
52
|
+
<CardContent className="py-12 text-center text-dark-muted">
|
|
53
|
+
No entry found.
|
|
54
|
+
</CardContent>
|
|
55
|
+
</Card>
|
|
56
|
+
</div>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const payload = entry.payload as Record<string, unknown>
|
|
61
|
+
const exitCode = Number(payload.exit_code ?? 0)
|
|
62
|
+
|
|
63
|
+
// Always include command in arguments like Telescope does
|
|
64
|
+
const rawArgs = (payload.arguments || {}) as Record<string, unknown>
|
|
65
|
+
const args = Object.keys(rawArgs).length > 0
|
|
66
|
+
? rawArgs
|
|
67
|
+
: { command: payload.command }
|
|
68
|
+
|
|
69
|
+
// Show all options including false/null values (like Telescope does)
|
|
70
|
+
const options = (payload.options || {}) as Record<string, unknown>
|
|
71
|
+
|
|
72
|
+
const formattedTime = new Date(entry.occurred_at).toLocaleString('en-US', {
|
|
73
|
+
year: 'numeric',
|
|
74
|
+
month: 'long',
|
|
75
|
+
day: 'numeric',
|
|
76
|
+
hour: 'numeric',
|
|
77
|
+
minute: '2-digit',
|
|
78
|
+
second: '2-digit',
|
|
79
|
+
hour12: true
|
|
80
|
+
})
|
|
81
|
+
const timeAgo = getTimeAgo(entry.occurred_at)
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div className="p-6 space-y-5">
|
|
85
|
+
{/* Command Details Card */}
|
|
86
|
+
<Card>
|
|
87
|
+
<CardHeader>
|
|
88
|
+
<CardTitle>Command Details</CardTitle>
|
|
89
|
+
</CardHeader>
|
|
90
|
+
<CardContent className="p-0">
|
|
91
|
+
<table className="w-full">
|
|
92
|
+
<tbody>
|
|
93
|
+
<tr className="border-t border-dark-border">
|
|
94
|
+
<td className="px-4 py-3 text-dark-muted whitespace-nowrap w-28">Time</td>
|
|
95
|
+
<td className="px-4 py-3">{formattedTime} ({timeAgo})</td>
|
|
96
|
+
</tr>
|
|
97
|
+
{payload.hostname ? (
|
|
98
|
+
<tr className="border-t border-dark-border">
|
|
99
|
+
<td className="px-4 py-3 text-dark-muted whitespace-nowrap">Hostname</td>
|
|
100
|
+
<td className="px-4 py-3">{String(payload.hostname)}</td>
|
|
101
|
+
</tr>
|
|
102
|
+
) : null}
|
|
103
|
+
<tr className="border-t border-dark-border">
|
|
104
|
+
<td className="px-4 py-3 text-dark-muted whitespace-nowrap">Command</td>
|
|
105
|
+
<td className="px-4 py-3">
|
|
106
|
+
<code className="text-blue-400">{String(payload.command)}</code>
|
|
107
|
+
</td>
|
|
108
|
+
</tr>
|
|
109
|
+
<tr className="border-t border-dark-border">
|
|
110
|
+
<td className="px-4 py-3 text-dark-muted whitespace-nowrap">Exit Code</td>
|
|
111
|
+
<td className="px-4 py-3">
|
|
112
|
+
<span className={exitCode === 0 ? 'text-green-400' : 'text-red-400'}>
|
|
113
|
+
{exitCode}
|
|
114
|
+
</span>
|
|
115
|
+
</td>
|
|
116
|
+
</tr>
|
|
117
|
+
{payload.duration ? (
|
|
118
|
+
<tr className="border-t border-dark-border">
|
|
119
|
+
<td className="px-4 py-3 text-dark-muted whitespace-nowrap">Duration</td>
|
|
120
|
+
<td className="px-4 py-3">{String(payload.duration)}ms</td>
|
|
121
|
+
</tr>
|
|
122
|
+
) : null}
|
|
123
|
+
{payload.description ? (
|
|
124
|
+
<tr className="border-t border-dark-border">
|
|
125
|
+
<td className="px-4 py-3 text-dark-muted whitespace-nowrap">Description</td>
|
|
126
|
+
<td className="px-4 py-3 text-dark-muted">{String(payload.description)}</td>
|
|
127
|
+
</tr>
|
|
128
|
+
) : null}
|
|
129
|
+
</tbody>
|
|
130
|
+
</table>
|
|
131
|
+
</CardContent>
|
|
132
|
+
</Card>
|
|
133
|
+
|
|
134
|
+
{/* Arguments / Options Card */}
|
|
135
|
+
<Card>
|
|
136
|
+
<div className="flex border-b border-dark-border">
|
|
137
|
+
<button
|
|
138
|
+
onClick={() => setCurrentTab('arguments')}
|
|
139
|
+
className={`px-4 py-2.5 text-sm font-medium ${
|
|
140
|
+
currentTab === 'arguments'
|
|
141
|
+
? 'bg-blue-500 text-white'
|
|
142
|
+
: 'text-dark-muted hover:text-dark-text'
|
|
143
|
+
}`}
|
|
144
|
+
>
|
|
145
|
+
Arguments ({Object.keys(args).length})
|
|
146
|
+
</button>
|
|
147
|
+
<button
|
|
148
|
+
onClick={() => setCurrentTab('options')}
|
|
149
|
+
className={`px-4 py-2.5 text-sm font-medium ${
|
|
150
|
+
currentTab === 'options'
|
|
151
|
+
? 'bg-blue-500 text-white'
|
|
152
|
+
: 'text-dark-muted hover:text-dark-text'
|
|
153
|
+
}`}
|
|
154
|
+
>
|
|
155
|
+
Options ({Object.keys(options).length})
|
|
156
|
+
</button>
|
|
157
|
+
</div>
|
|
158
|
+
<div className="bg-[#1a1a2e] p-4">
|
|
159
|
+
{currentTab === 'arguments' && Object.keys(args).length === 0 ? (
|
|
160
|
+
<div className="text-dark-muted text-sm">No arguments provided</div>
|
|
161
|
+
) : currentTab === 'options' && Object.keys(options).length === 0 ? (
|
|
162
|
+
<div className="text-dark-muted text-sm">No options provided</div>
|
|
163
|
+
) : (
|
|
164
|
+
<JsonViewer
|
|
165
|
+
data={currentTab === 'arguments' ? args : options}
|
|
166
|
+
className="border-0 bg-transparent"
|
|
167
|
+
/>
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
</Card>
|
|
171
|
+
|
|
172
|
+
{/* Related Entries (batch) - exceptions will appear here */}
|
|
173
|
+
{batch.length > 0 && <RelatedEntries entries={batch} navigate={navigate} />}
|
|
174
|
+
</div>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function getTimeAgo(date: string): string {
|
|
179
|
+
const seconds = Math.floor((new Date().getTime() - new Date(date).getTime()) / 1000)
|
|
180
|
+
if (seconds < 60) return `${seconds}s ago`
|
|
181
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`
|
|
182
|
+
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`
|
|
183
|
+
return `${Math.floor(seconds / 86400)}d ago`
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
interface RelatedEntriesProps {
|
|
187
|
+
entries: Entry[]
|
|
188
|
+
navigate: ReturnType<typeof useNavigate>
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function RelatedEntries({ entries, navigate }: RelatedEntriesProps) {
|
|
192
|
+
const [currentTab, setCurrentTab] = useState<string>('')
|
|
193
|
+
|
|
194
|
+
const groupedEntries = entries.reduce((acc, entry) => {
|
|
195
|
+
const type = entry.entry_type
|
|
196
|
+
if (!acc[type]) acc[type] = []
|
|
197
|
+
acc[type].push(entry)
|
|
198
|
+
return acc
|
|
199
|
+
}, {} as Record<string, Entry[]>)
|
|
200
|
+
|
|
201
|
+
const tabs = Object.entries(groupedEntries).map(([type, items]) => ({
|
|
202
|
+
type,
|
|
203
|
+
label: getTypeLabel(type),
|
|
204
|
+
count: items.length
|
|
205
|
+
}))
|
|
206
|
+
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
if (tabs.length > 0 && !currentTab) {
|
|
209
|
+
setCurrentTab(tabs[0].type)
|
|
210
|
+
}
|
|
211
|
+
}, [tabs, currentTab])
|
|
212
|
+
|
|
213
|
+
if (tabs.length === 0) return null
|
|
214
|
+
|
|
215
|
+
const currentEntries = groupedEntries[currentTab] || []
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<Card>
|
|
219
|
+
<div className="flex flex-wrap border-b border-dark-border">
|
|
220
|
+
{tabs.map((tab) => (
|
|
221
|
+
<button
|
|
222
|
+
key={tab.type}
|
|
223
|
+
onClick={() => setCurrentTab(tab.type)}
|
|
224
|
+
className={`px-4 py-2.5 text-sm font-medium ${
|
|
225
|
+
currentTab === tab.type
|
|
226
|
+
? 'bg-blue-500 text-white'
|
|
227
|
+
: 'text-dark-muted hover:text-dark-text'
|
|
228
|
+
}`}
|
|
229
|
+
>
|
|
230
|
+
{tab.label} ({tab.count})
|
|
231
|
+
</button>
|
|
232
|
+
))}
|
|
233
|
+
</div>
|
|
234
|
+
<table className="w-full">
|
|
235
|
+
<thead>
|
|
236
|
+
<tr className="border-b border-dark-border">
|
|
237
|
+
{currentTab === 'query' && (
|
|
238
|
+
<>
|
|
239
|
+
<th className="px-4 py-3 text-left text-sm font-medium text-dark-muted">Query</th>
|
|
240
|
+
<th className="px-4 py-3 text-right text-sm font-medium text-dark-muted">Duration</th>
|
|
241
|
+
<th className="w-12"></th>
|
|
242
|
+
</>
|
|
243
|
+
)}
|
|
244
|
+
{currentTab === 'exception' && (
|
|
245
|
+
<>
|
|
246
|
+
<th className="px-4 py-3 text-left text-sm font-medium text-dark-muted">Message</th>
|
|
247
|
+
<th className="w-12"></th>
|
|
248
|
+
</>
|
|
249
|
+
)}
|
|
250
|
+
{(currentTab === 'job_enqueue' || currentTab === 'job_perform') && (
|
|
251
|
+
<>
|
|
252
|
+
<th className="px-4 py-3 text-left text-sm font-medium text-dark-muted">Job</th>
|
|
253
|
+
<th className="px-4 py-3 text-left text-sm font-medium text-dark-muted">Status</th>
|
|
254
|
+
<th className="w-12"></th>
|
|
255
|
+
</>
|
|
256
|
+
)}
|
|
257
|
+
{!['query', 'exception', 'job_enqueue', 'job_perform'].includes(currentTab) && (
|
|
258
|
+
<>
|
|
259
|
+
<th className="px-4 py-3 text-left text-sm font-medium text-dark-muted">Entry</th>
|
|
260
|
+
<th className="w-12"></th>
|
|
261
|
+
</>
|
|
262
|
+
)}
|
|
263
|
+
</tr>
|
|
264
|
+
</thead>
|
|
265
|
+
<tbody>
|
|
266
|
+
{currentEntries.map((entry) => {
|
|
267
|
+
const payload = entry.payload as Record<string, unknown>
|
|
268
|
+
const path = getEntryPath(entry)
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<tr
|
|
272
|
+
key={entry.id}
|
|
273
|
+
onClick={() => navigate(path)}
|
|
274
|
+
className="border-b border-dark-border hover:bg-white/[0.02] cursor-pointer"
|
|
275
|
+
>
|
|
276
|
+
{currentTab === 'query' && (
|
|
277
|
+
<>
|
|
278
|
+
<td className="px-4 py-3" title={String(payload.sql)}>
|
|
279
|
+
<code className="text-xs text-dark-muted">
|
|
280
|
+
{String(payload.sql || '').substring(0, 110)}
|
|
281
|
+
</code>
|
|
282
|
+
</td>
|
|
283
|
+
<td className="px-4 py-3 text-right whitespace-nowrap">
|
|
284
|
+
{entry.tags.includes('slow') ? (
|
|
285
|
+
<span className="px-2 py-0.5 bg-red-500/20 text-red-400 text-xs rounded">
|
|
286
|
+
{String(payload.duration)}ms
|
|
287
|
+
</span>
|
|
288
|
+
) : (
|
|
289
|
+
<span className="text-dark-muted">{String(payload.duration)}ms</span>
|
|
290
|
+
)}
|
|
291
|
+
</td>
|
|
292
|
+
<td className="px-4 py-3">
|
|
293
|
+
<ArrowIcon />
|
|
294
|
+
</td>
|
|
295
|
+
</>
|
|
296
|
+
)}
|
|
297
|
+
{currentTab === 'exception' && (
|
|
298
|
+
<>
|
|
299
|
+
<td className="px-4 py-3" title={String(payload.class)}>
|
|
300
|
+
<span>{String(payload.class)}</span>
|
|
301
|
+
<br />
|
|
302
|
+
<small className="text-dark-muted">
|
|
303
|
+
{String(payload.message || '').substring(0, 200)}
|
|
304
|
+
</small>
|
|
305
|
+
</td>
|
|
306
|
+
<td className="px-4 py-3">
|
|
307
|
+
<ArrowIcon />
|
|
308
|
+
</td>
|
|
309
|
+
</>
|
|
310
|
+
)}
|
|
311
|
+
{(currentTab === 'job_enqueue' || currentTab === 'job_perform') && (
|
|
312
|
+
<>
|
|
313
|
+
<td className="px-4 py-3">
|
|
314
|
+
<span title={String(payload.job_class)}>
|
|
315
|
+
{String(payload.job_class)}
|
|
316
|
+
</span>
|
|
317
|
+
<br />
|
|
318
|
+
<small className="text-dark-muted">
|
|
319
|
+
Queue: {String(payload.queue_name)}
|
|
320
|
+
</small>
|
|
321
|
+
</td>
|
|
322
|
+
<td className="px-4 py-3">
|
|
323
|
+
<span className={`px-2 py-0.5 text-xs rounded ${
|
|
324
|
+
entry.tags.includes('failed')
|
|
325
|
+
? 'bg-red-500/20 text-red-400'
|
|
326
|
+
: 'bg-green-500/20 text-green-400'
|
|
327
|
+
}`}>
|
|
328
|
+
{entry.tags.includes('failed') ? 'failed' : 'processed'}
|
|
329
|
+
</span>
|
|
330
|
+
</td>
|
|
331
|
+
<td className="px-4 py-3">
|
|
332
|
+
<ArrowIcon />
|
|
333
|
+
</td>
|
|
334
|
+
</>
|
|
335
|
+
)}
|
|
336
|
+
{!['query', 'exception', 'job_enqueue', 'job_perform'].includes(currentTab) && (
|
|
337
|
+
<>
|
|
338
|
+
<td className="px-4 py-3 text-dark-muted">{entry.entry_type}</td>
|
|
339
|
+
<td className="px-4 py-3">
|
|
340
|
+
<ArrowIcon />
|
|
341
|
+
</td>
|
|
342
|
+
</>
|
|
343
|
+
)}
|
|
344
|
+
</tr>
|
|
345
|
+
)
|
|
346
|
+
})}
|
|
347
|
+
</tbody>
|
|
348
|
+
</table>
|
|
349
|
+
</Card>
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function ArrowIcon() {
|
|
354
|
+
return (
|
|
355
|
+
<svg className="w-5 h-5 text-dark-muted" viewBox="0 0 20 20" fill="currentColor">
|
|
356
|
+
<path
|
|
357
|
+
fillRule="evenodd"
|
|
358
|
+
d="M10 18a8 8 0 100-16 8 8 0 000 16zM6.75 9.25a.75.75 0 000 1.5h4.59l-2.1 1.95a.75.75 0 001.02 1.1l3.5-3.25a.75.75 0 000-1.1l-3.5-3.25a.75.75 0 10-1.02 1.1l2.1 1.95H6.75z"
|
|
359
|
+
clipRule="evenodd"
|
|
360
|
+
/>
|
|
361
|
+
</svg>
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function getEntryPath(entry: Entry): string {
|
|
366
|
+
switch (entry.entry_type) {
|
|
367
|
+
case 'query': return `/queries/${entry.id}`
|
|
368
|
+
case 'exception': return `/exceptions/${entry.id}`
|
|
369
|
+
case 'job_enqueue':
|
|
370
|
+
case 'job_perform': return `/jobs/${entry.id}`
|
|
371
|
+
case 'request': return `/requests/${entry.id}`
|
|
372
|
+
default: return `/${entry.entry_type}s/${entry.id}`
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function getTypeLabel(type: string): string {
|
|
377
|
+
const labels: Record<string, string> = {
|
|
378
|
+
query: 'Queries',
|
|
379
|
+
exception: 'Exceptions',
|
|
380
|
+
job_enqueue: 'Jobs',
|
|
381
|
+
job_perform: 'Jobs',
|
|
382
|
+
request: 'Requests',
|
|
383
|
+
log: 'Logs',
|
|
384
|
+
cache: 'Cache',
|
|
385
|
+
event: 'Events',
|
|
386
|
+
mail: 'Mail',
|
|
387
|
+
notification: 'Notifications',
|
|
388
|
+
model: 'Models',
|
|
389
|
+
gate: 'Gates',
|
|
390
|
+
redis: 'Redis',
|
|
391
|
+
view: 'Views',
|
|
392
|
+
client_request: 'HTTP Client'
|
|
393
|
+
}
|
|
394
|
+
return labels[type] || type
|
|
395
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import PlaceholderPage from '@/components/PlaceholderPage'
|
|
2
|
+
|
|
3
|
+
export default function DumpsIndex() {
|
|
4
|
+
return (
|
|
5
|
+
<PlaceholderPage
|
|
6
|
+
title="Dumps"
|
|
7
|
+
description="Debug dumps from puts, pp, and Rails.logger.debug calls"
|
|
8
|
+
icon={
|
|
9
|
+
<svg className="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
10
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
|
|
11
|
+
</svg>
|
|
12
|
+
}
|
|
13
|
+
/>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import PlaceholderPage from '@/components/PlaceholderPage'
|
|
2
|
+
|
|
3
|
+
export default function EventsIndex() {
|
|
4
|
+
return (
|
|
5
|
+
<PlaceholderPage
|
|
6
|
+
title="Events"
|
|
7
|
+
description="ActiveSupport::Notifications events in your application"
|
|
8
|
+
icon={
|
|
9
|
+
<svg className="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
10
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
11
|
+
</svg>
|
|
12
|
+
}
|
|
13
|
+
/>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { useNavigate, useSearchParams } from 'react-router-dom'
|
|
3
|
+
import { getEntries, getFamilyEntries } from '@/api/entries'
|
|
4
|
+
import { Entry } from '@/lib/types'
|
|
5
|
+
import { timeAgo, truncate } from '@/lib/utils'
|
|
6
|
+
import { Card } from '@/components/ui/Card'
|
|
7
|
+
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@/components/ui/Table'
|
|
8
|
+
import { Pagination } from '@/components/ui/Pagination'
|
|
9
|
+
import { SearchInput } from '@/components/ui/SearchInput'
|
|
10
|
+
|
|
11
|
+
export default function ExceptionsIndex() {
|
|
12
|
+
const navigate = useNavigate()
|
|
13
|
+
const [searchParams, setSearchParams] = useSearchParams()
|
|
14
|
+
const [entries, setEntries] = useState<Entry[]>([])
|
|
15
|
+
const [loading, setLoading] = useState(true)
|
|
16
|
+
const [page, setPage] = useState(1)
|
|
17
|
+
const [totalPages, setTotalPages] = useState(1)
|
|
18
|
+
|
|
19
|
+
const tagFilter = searchParams.get('tag') || ''
|
|
20
|
+
const familyHashFilter = searchParams.get('family_hash') || ''
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
loadEntries()
|
|
24
|
+
}, [page, tagFilter, familyHashFilter])
|
|
25
|
+
|
|
26
|
+
async function loadEntries() {
|
|
27
|
+
setLoading(true)
|
|
28
|
+
try {
|
|
29
|
+
if (familyHashFilter) {
|
|
30
|
+
// Use family endpoint for "View Other Occurrences"
|
|
31
|
+
const response = await getFamilyEntries(familyHashFilter, page)
|
|
32
|
+
setEntries(response.data)
|
|
33
|
+
setTotalPages(response.meta.total_pages)
|
|
34
|
+
} else {
|
|
35
|
+
const response = await getEntries({
|
|
36
|
+
type: 'exception',
|
|
37
|
+
tag: tagFilter || undefined,
|
|
38
|
+
page
|
|
39
|
+
})
|
|
40
|
+
setEntries(response.data)
|
|
41
|
+
setTotalPages(response.meta.total_pages)
|
|
42
|
+
}
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('Failed to load entries:', error)
|
|
45
|
+
} finally {
|
|
46
|
+
setLoading(false)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function handleSearch(value: string) {
|
|
51
|
+
setPage(1)
|
|
52
|
+
if (value) {
|
|
53
|
+
setSearchParams({ tag: value })
|
|
54
|
+
} else {
|
|
55
|
+
setSearchParams({})
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function handlePageChange(newPage: number) {
|
|
60
|
+
setPage(newPage)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function clearFamilyFilter() {
|
|
64
|
+
setSearchParams({})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className="p-6">
|
|
69
|
+
<div className="mb-6">
|
|
70
|
+
<h1 className="text-2xl font-semibold text-white">Exceptions</h1>
|
|
71
|
+
<p className="text-dark-muted text-sm mt-1">
|
|
72
|
+
{familyHashFilter
|
|
73
|
+
? 'Viewing all occurrences of this exception'
|
|
74
|
+
: 'Unhandled exceptions in your application'}
|
|
75
|
+
</p>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
{familyHashFilter ? (
|
|
79
|
+
<div className="mb-4">
|
|
80
|
+
<button
|
|
81
|
+
onClick={clearFamilyFilter}
|
|
82
|
+
className="text-blue-400 hover:text-blue-300 text-sm"
|
|
83
|
+
>
|
|
84
|
+
← Back to all exceptions
|
|
85
|
+
</button>
|
|
86
|
+
</div>
|
|
87
|
+
) : (
|
|
88
|
+
<div className="mb-4">
|
|
89
|
+
<SearchInput
|
|
90
|
+
placeholder="Search by tag..."
|
|
91
|
+
value={tagFilter}
|
|
92
|
+
onChange={handleSearch}
|
|
93
|
+
className="max-w-sm"
|
|
94
|
+
/>
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
97
|
+
|
|
98
|
+
<Card>
|
|
99
|
+
<Table>
|
|
100
|
+
<TableHeader>
|
|
101
|
+
<TableRow>
|
|
102
|
+
<TableHead>Exception</TableHead>
|
|
103
|
+
<TableHead>Location</TableHead>
|
|
104
|
+
<TableHead>Happened</TableHead>
|
|
105
|
+
</TableRow>
|
|
106
|
+
</TableHeader>
|
|
107
|
+
<TableBody>
|
|
108
|
+
{loading ? (
|
|
109
|
+
<TableRow>
|
|
110
|
+
<TableCell className="text-center text-dark-muted py-8" colSpan={3}>
|
|
111
|
+
Loading...
|
|
112
|
+
</TableCell>
|
|
113
|
+
</TableRow>
|
|
114
|
+
) : entries.length === 0 ? (
|
|
115
|
+
<TableRow>
|
|
116
|
+
<TableCell className="text-center text-dark-muted py-8" colSpan={3}>
|
|
117
|
+
{familyHashFilter
|
|
118
|
+
? 'No other occurrences found.'
|
|
119
|
+
: tagFilter
|
|
120
|
+
? `No exceptions found with tag "${tagFilter}".`
|
|
121
|
+
: "No exceptions recorded. That's a good thing!"}
|
|
122
|
+
</TableCell>
|
|
123
|
+
</TableRow>
|
|
124
|
+
) : (
|
|
125
|
+
entries.map((entry) => {
|
|
126
|
+
const payload = entry.payload as Record<string, unknown>
|
|
127
|
+
// Show file:line as location (like Telescope)
|
|
128
|
+
const fileLocation = payload.file && payload.line
|
|
129
|
+
? `${String(payload.file)}:${payload.line}`
|
|
130
|
+
: null
|
|
131
|
+
return (
|
|
132
|
+
<TableRow key={entry.id} onClick={() => navigate(`/exceptions/${entry.id}`)}>
|
|
133
|
+
<TableCell>
|
|
134
|
+
<div className="text-red-400 font-medium">{String(payload.class)}</div>
|
|
135
|
+
<div className="text-dark-muted text-sm">
|
|
136
|
+
{truncate(String(payload.message), 60)}
|
|
137
|
+
</div>
|
|
138
|
+
</TableCell>
|
|
139
|
+
<TableCell className="font-mono text-xs text-dark-muted">
|
|
140
|
+
{fileLocation ? truncate(fileLocation, 50) : '-'}
|
|
141
|
+
</TableCell>
|
|
142
|
+
<TableCell className="text-dark-muted" title={entry.occurred_at}>
|
|
143
|
+
{timeAgo(entry.occurred_at)}
|
|
144
|
+
</TableCell>
|
|
145
|
+
</TableRow>
|
|
146
|
+
)
|
|
147
|
+
})
|
|
148
|
+
)}
|
|
149
|
+
</TableBody>
|
|
150
|
+
</Table>
|
|
151
|
+
<Pagination currentPage={page} totalPages={totalPages} onPageChange={handlePageChange} />
|
|
152
|
+
</Card>
|
|
153
|
+
</div>
|
|
154
|
+
)
|
|
155
|
+
}
|