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 { MethodBadge, StatusBadge } from '@/components/ui/Badge'
|
|
7
|
+
import { JsonViewer } from '@/components/ui/JsonViewer'
|
|
8
|
+
|
|
9
|
+
type TabType = 'payload' | 'headers'
|
|
10
|
+
type ResponseTabType = 'response' | 'response_headers' | 'session'
|
|
11
|
+
|
|
12
|
+
export default function RequestsShow() {
|
|
13
|
+
const { id } = useParams()
|
|
14
|
+
const navigate = useNavigate()
|
|
15
|
+
const [entry, setEntry] = useState<Entry | null>(null)
|
|
16
|
+
const [batch, setBatch] = useState<Entry[]>([])
|
|
17
|
+
const [loading, setLoading] = useState(true)
|
|
18
|
+
const [requestTab, setRequestTab] = useState<TabType>('payload')
|
|
19
|
+
const [responseTab, setResponseTab] = useState<ResponseTabType>('response')
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
loadEntry()
|
|
23
|
+
}, [id])
|
|
24
|
+
|
|
25
|
+
async function loadEntry() {
|
|
26
|
+
if (!id) return
|
|
27
|
+
setLoading(true)
|
|
28
|
+
try {
|
|
29
|
+
const response = await getEntry(id)
|
|
30
|
+
setEntry(response.data)
|
|
31
|
+
setBatch(response.batch || [])
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error('Failed to load entry:', error)
|
|
34
|
+
} finally {
|
|
35
|
+
setLoading(false)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (loading) {
|
|
40
|
+
return (
|
|
41
|
+
<div className="p-6">
|
|
42
|
+
<Card>
|
|
43
|
+
<CardContent className="py-12 text-center text-dark-muted">
|
|
44
|
+
Loading...
|
|
45
|
+
</CardContent>
|
|
46
|
+
</Card>
|
|
47
|
+
</div>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!entry) {
|
|
52
|
+
return (
|
|
53
|
+
<div className="p-6">
|
|
54
|
+
<Card>
|
|
55
|
+
<CardContent className="py-12 text-center text-dark-muted">
|
|
56
|
+
Request not found.
|
|
57
|
+
</CardContent>
|
|
58
|
+
</Card>
|
|
59
|
+
</div>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const payload = entry.payload as Record<string, unknown>
|
|
64
|
+
|
|
65
|
+
const formattedTime = new Date(entry.occurred_at).toLocaleString('en-US', {
|
|
66
|
+
year: 'numeric',
|
|
67
|
+
month: 'long',
|
|
68
|
+
day: 'numeric',
|
|
69
|
+
hour: 'numeric',
|
|
70
|
+
minute: '2-digit',
|
|
71
|
+
second: '2-digit',
|
|
72
|
+
hour12: true
|
|
73
|
+
})
|
|
74
|
+
const timeAgoStr = getTimeAgo(entry.occurred_at)
|
|
75
|
+
|
|
76
|
+
// Get tab content
|
|
77
|
+
const getRequestTabContent = () => {
|
|
78
|
+
if (requestTab === 'payload') {
|
|
79
|
+
return payload.payload || {}
|
|
80
|
+
}
|
|
81
|
+
return payload.headers || {}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const getResponseTabContent = () => {
|
|
85
|
+
if (responseTab === 'response') {
|
|
86
|
+
return payload.response || {}
|
|
87
|
+
}
|
|
88
|
+
if (responseTab === 'response_headers') {
|
|
89
|
+
return payload.response_headers || {}
|
|
90
|
+
}
|
|
91
|
+
return payload.session || {}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div className="p-6 space-y-5">
|
|
96
|
+
{/* Request Details Card */}
|
|
97
|
+
<Card>
|
|
98
|
+
<CardHeader>
|
|
99
|
+
<CardTitle>Request Details</CardTitle>
|
|
100
|
+
</CardHeader>
|
|
101
|
+
<CardContent className="p-0">
|
|
102
|
+
<table className="w-full">
|
|
103
|
+
<tbody>
|
|
104
|
+
<tr className="border-t border-dark-border">
|
|
105
|
+
<td className="px-4 py-3 text-dark-muted whitespace-nowrap w-32">Time</td>
|
|
106
|
+
<td className="px-4 py-3">{formattedTime} ({timeAgoStr})</td>
|
|
107
|
+
</tr>
|
|
108
|
+
<tr className="border-t border-dark-border">
|
|
109
|
+
<td className="px-4 py-3 text-dark-muted whitespace-nowrap">Method</td>
|
|
110
|
+
<td className="px-4 py-3">
|
|
111
|
+
<MethodBadge method={String(payload.method)} />
|
|
112
|
+
</td>
|
|
113
|
+
</tr>
|
|
114
|
+
<tr className="border-t border-dark-border">
|
|
115
|
+
<td className="px-4 py-3 text-dark-muted whitespace-nowrap">Controller Action</td>
|
|
116
|
+
<td className="px-4 py-3 font-mono text-sm">
|
|
117
|
+
{String(payload.controller_action || `${payload.controller}@${payload.action}`)}
|
|
118
|
+
</td>
|
|
119
|
+
</tr>
|
|
120
|
+
<tr className="border-t border-dark-border">
|
|
121
|
+
<td className="px-4 py-3 text-dark-muted whitespace-nowrap">Path</td>
|
|
122
|
+
<td className="px-4 py-3 font-mono text-sm">{String(payload.path)}</td>
|
|
123
|
+
</tr>
|
|
124
|
+
<tr className="border-t border-dark-border">
|
|
125
|
+
<td className="px-4 py-3 text-dark-muted whitespace-nowrap">Status</td>
|
|
126
|
+
<td className="px-4 py-3">
|
|
127
|
+
<StatusBadge status={Number(payload.status)} />
|
|
128
|
+
</td>
|
|
129
|
+
</tr>
|
|
130
|
+
<tr className="border-t border-dark-border">
|
|
131
|
+
<td className="px-4 py-3 text-dark-muted whitespace-nowrap">Duration</td>
|
|
132
|
+
<td className="px-4 py-3">{String(payload.duration ?? '-')}ms</td>
|
|
133
|
+
</tr>
|
|
134
|
+
{payload.ip_address ? (
|
|
135
|
+
<tr className="border-t border-dark-border">
|
|
136
|
+
<td className="px-4 py-3 text-dark-muted whitespace-nowrap">IP Address</td>
|
|
137
|
+
<td className="px-4 py-3">{String(payload.ip_address)}</td>
|
|
138
|
+
</tr>
|
|
139
|
+
) : null}
|
|
140
|
+
{payload.hostname ? (
|
|
141
|
+
<tr className="border-t border-dark-border">
|
|
142
|
+
<td className="px-4 py-3 text-dark-muted whitespace-nowrap">Hostname</td>
|
|
143
|
+
<td className="px-4 py-3 font-mono text-sm">{String(payload.hostname)}</td>
|
|
144
|
+
</tr>
|
|
145
|
+
) : null}
|
|
146
|
+
{payload.db_runtime ? (
|
|
147
|
+
<tr className="border-t border-dark-border">
|
|
148
|
+
<td className="px-4 py-3 text-dark-muted whitespace-nowrap">DB Runtime</td>
|
|
149
|
+
<td className="px-4 py-3">{String(payload.db_runtime)}ms</td>
|
|
150
|
+
</tr>
|
|
151
|
+
) : null}
|
|
152
|
+
{payload.view_runtime ? (
|
|
153
|
+
<tr className="border-t border-dark-border">
|
|
154
|
+
<td className="px-4 py-3 text-dark-muted whitespace-nowrap">View Runtime</td>
|
|
155
|
+
<td className="px-4 py-3">{String(payload.view_runtime)}ms</td>
|
|
156
|
+
</tr>
|
|
157
|
+
) : null}
|
|
158
|
+
</tbody>
|
|
159
|
+
</table>
|
|
160
|
+
</CardContent>
|
|
161
|
+
</Card>
|
|
162
|
+
|
|
163
|
+
{/* Request Card with Tabs */}
|
|
164
|
+
<Card>
|
|
165
|
+
<div className="flex border-b border-dark-border">
|
|
166
|
+
<button
|
|
167
|
+
onClick={() => setRequestTab('payload')}
|
|
168
|
+
className={`px-4 py-2.5 text-sm font-medium ${
|
|
169
|
+
requestTab === 'payload'
|
|
170
|
+
? 'bg-blue-500 text-white'
|
|
171
|
+
: 'text-dark-muted hover:text-dark-text'
|
|
172
|
+
}`}
|
|
173
|
+
>
|
|
174
|
+
Payload
|
|
175
|
+
</button>
|
|
176
|
+
<button
|
|
177
|
+
onClick={() => setRequestTab('headers')}
|
|
178
|
+
className={`px-4 py-2.5 text-sm font-medium ${
|
|
179
|
+
requestTab === 'headers'
|
|
180
|
+
? 'bg-blue-500 text-white'
|
|
181
|
+
: 'text-dark-muted hover:text-dark-text'
|
|
182
|
+
}`}
|
|
183
|
+
>
|
|
184
|
+
Headers
|
|
185
|
+
</button>
|
|
186
|
+
</div>
|
|
187
|
+
<div className="bg-[#1a1a2e]">
|
|
188
|
+
<JsonViewer data={getRequestTabContent()} className="border-0 bg-transparent" />
|
|
189
|
+
</div>
|
|
190
|
+
</Card>
|
|
191
|
+
|
|
192
|
+
{/* Response Card with Tabs */}
|
|
193
|
+
<Card>
|
|
194
|
+
<div className="flex border-b border-dark-border">
|
|
195
|
+
<button
|
|
196
|
+
onClick={() => setResponseTab('response')}
|
|
197
|
+
className={`px-4 py-2.5 text-sm font-medium ${
|
|
198
|
+
responseTab === 'response'
|
|
199
|
+
? 'bg-blue-500 text-white'
|
|
200
|
+
: 'text-dark-muted hover:text-dark-text'
|
|
201
|
+
}`}
|
|
202
|
+
>
|
|
203
|
+
Response
|
|
204
|
+
</button>
|
|
205
|
+
<button
|
|
206
|
+
onClick={() => setResponseTab('response_headers')}
|
|
207
|
+
className={`px-4 py-2.5 text-sm font-medium ${
|
|
208
|
+
responseTab === 'response_headers'
|
|
209
|
+
? 'bg-blue-500 text-white'
|
|
210
|
+
: 'text-dark-muted hover:text-dark-text'
|
|
211
|
+
}`}
|
|
212
|
+
>
|
|
213
|
+
Headers
|
|
214
|
+
</button>
|
|
215
|
+
<button
|
|
216
|
+
onClick={() => setResponseTab('session')}
|
|
217
|
+
className={`px-4 py-2.5 text-sm font-medium ${
|
|
218
|
+
responseTab === 'session'
|
|
219
|
+
? 'bg-blue-500 text-white'
|
|
220
|
+
: 'text-dark-muted hover:text-dark-text'
|
|
221
|
+
}`}
|
|
222
|
+
>
|
|
223
|
+
Session
|
|
224
|
+
</button>
|
|
225
|
+
</div>
|
|
226
|
+
<div className="bg-[#1a1a2e]">
|
|
227
|
+
<JsonViewer data={getResponseTabContent()} className="border-0 bg-transparent" />
|
|
228
|
+
</div>
|
|
229
|
+
</Card>
|
|
230
|
+
|
|
231
|
+
{/* Related Entries */}
|
|
232
|
+
{batch.length > 0 && <RelatedEntries entries={batch} currentEntryId={entry.id} navigate={navigate} />}
|
|
233
|
+
</div>
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function getTimeAgo(date: string): string {
|
|
238
|
+
const seconds = Math.floor((new Date().getTime() - new Date(date).getTime()) / 1000)
|
|
239
|
+
if (seconds < 60) return `${seconds}s ago`
|
|
240
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`
|
|
241
|
+
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`
|
|
242
|
+
return `${Math.floor(seconds / 86400)}d ago`
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
interface RelatedEntriesProps {
|
|
246
|
+
entries: Entry[]
|
|
247
|
+
currentEntryId: number
|
|
248
|
+
navigate: ReturnType<typeof useNavigate>
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function RelatedEntries({ entries, currentEntryId, navigate }: RelatedEntriesProps) {
|
|
252
|
+
const [currentTab, setCurrentTab] = useState<string>('')
|
|
253
|
+
|
|
254
|
+
// Filter out the current entry from related entries
|
|
255
|
+
const filteredEntries = entries.filter(e => e.id !== currentEntryId)
|
|
256
|
+
|
|
257
|
+
const groupedEntries = filteredEntries.reduce((acc, entry) => {
|
|
258
|
+
const type = entry.entry_type
|
|
259
|
+
if (!acc[type]) acc[type] = []
|
|
260
|
+
acc[type].push(entry)
|
|
261
|
+
return acc
|
|
262
|
+
}, {} as Record<string, Entry[]>)
|
|
263
|
+
|
|
264
|
+
const tabs = Object.entries(groupedEntries).map(([type, items]) => ({
|
|
265
|
+
type,
|
|
266
|
+
label: getTypeLabel(type),
|
|
267
|
+
count: items.length
|
|
268
|
+
}))
|
|
269
|
+
|
|
270
|
+
useEffect(() => {
|
|
271
|
+
if (tabs.length > 0 && !currentTab) {
|
|
272
|
+
setCurrentTab(tabs[0].type)
|
|
273
|
+
}
|
|
274
|
+
}, [tabs.length])
|
|
275
|
+
|
|
276
|
+
if (tabs.length === 0) return null
|
|
277
|
+
|
|
278
|
+
const currentEntries = groupedEntries[currentTab] || []
|
|
279
|
+
|
|
280
|
+
return (
|
|
281
|
+
<Card>
|
|
282
|
+
<CardHeader>
|
|
283
|
+
<CardTitle>Related Entries</CardTitle>
|
|
284
|
+
</CardHeader>
|
|
285
|
+
<div className="flex flex-wrap border-b border-dark-border">
|
|
286
|
+
{tabs.map((tab) => (
|
|
287
|
+
<button
|
|
288
|
+
key={tab.type}
|
|
289
|
+
onClick={() => setCurrentTab(tab.type)}
|
|
290
|
+
className={`px-4 py-2.5 text-sm font-medium ${
|
|
291
|
+
currentTab === tab.type
|
|
292
|
+
? 'bg-blue-500 text-white'
|
|
293
|
+
: 'text-dark-muted hover:text-dark-text'
|
|
294
|
+
}`}
|
|
295
|
+
>
|
|
296
|
+
{tab.label} ({tab.count})
|
|
297
|
+
</button>
|
|
298
|
+
))}
|
|
299
|
+
</div>
|
|
300
|
+
<table className="w-full">
|
|
301
|
+
<tbody>
|
|
302
|
+
{currentEntries.map((relatedEntry) => {
|
|
303
|
+
const relPayload = relatedEntry.payload as Record<string, unknown>
|
|
304
|
+
const path = getEntryPath(relatedEntry)
|
|
305
|
+
|
|
306
|
+
return (
|
|
307
|
+
<tr
|
|
308
|
+
key={relatedEntry.id}
|
|
309
|
+
onClick={() => navigate(path)}
|
|
310
|
+
className="border-b border-dark-border hover:bg-white/[0.02] cursor-pointer"
|
|
311
|
+
>
|
|
312
|
+
<td className="px-4 py-3">
|
|
313
|
+
<span className="text-dark-text text-sm">{getEntryDescription(relatedEntry, relPayload)}</span>
|
|
314
|
+
</td>
|
|
315
|
+
<td className="px-4 py-3 text-right text-dark-muted text-xs">
|
|
316
|
+
{getTimeAgo(relatedEntry.occurred_at)}
|
|
317
|
+
</td>
|
|
318
|
+
<td className="px-4 py-3 w-12">
|
|
319
|
+
<ArrowIcon />
|
|
320
|
+
</td>
|
|
321
|
+
</tr>
|
|
322
|
+
)
|
|
323
|
+
})}
|
|
324
|
+
</tbody>
|
|
325
|
+
</table>
|
|
326
|
+
</Card>
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function ArrowIcon() {
|
|
331
|
+
return (
|
|
332
|
+
<svg className="w-5 h-5 text-dark-muted" viewBox="0 0 20 20" fill="currentColor">
|
|
333
|
+
<path
|
|
334
|
+
fillRule="evenodd"
|
|
335
|
+
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"
|
|
336
|
+
clipRule="evenodd"
|
|
337
|
+
/>
|
|
338
|
+
</svg>
|
|
339
|
+
)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function getEntryPath(entry: Entry): string {
|
|
343
|
+
switch (entry.entry_type) {
|
|
344
|
+
case 'query': return `/queries/${entry.id}`
|
|
345
|
+
case 'exception': return `/exceptions/${entry.id}`
|
|
346
|
+
case 'job_enqueue':
|
|
347
|
+
case 'job_perform': return `/jobs/${entry.id}`
|
|
348
|
+
case 'request': return `/requests/${entry.id}`
|
|
349
|
+
case 'command': return `/commands/${entry.id}`
|
|
350
|
+
case 'view': return `/views/${entry.id}`
|
|
351
|
+
default: return `/${entry.entry_type}s/${entry.id}`
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function getEntryDescription(entry: Entry, payload: Record<string, unknown>): string {
|
|
356
|
+
switch (entry.entry_type) {
|
|
357
|
+
case 'query':
|
|
358
|
+
return String(payload.sql || '').substring(0, 100)
|
|
359
|
+
case 'request':
|
|
360
|
+
return `${payload.method} ${payload.path}`
|
|
361
|
+
case 'command':
|
|
362
|
+
return `Command: ${payload.command}`
|
|
363
|
+
case 'exception':
|
|
364
|
+
return `${payload.class}: ${String(payload.message || '').substring(0, 50)}`
|
|
365
|
+
case 'job_enqueue':
|
|
366
|
+
case 'job_perform':
|
|
367
|
+
return `Job: ${payload.job_class}`
|
|
368
|
+
case 'view':
|
|
369
|
+
return `${payload.view_type}: ${payload.name || payload.path}`
|
|
370
|
+
default:
|
|
371
|
+
return entry.entry_type
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function getTypeLabel(type: string): string {
|
|
376
|
+
const labels: Record<string, string> = {
|
|
377
|
+
query: 'Queries',
|
|
378
|
+
exception: 'Exceptions',
|
|
379
|
+
job_enqueue: 'Enqueued',
|
|
380
|
+
job_perform: 'Performed',
|
|
381
|
+
request: 'Requests',
|
|
382
|
+
command: 'Commands',
|
|
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 ScheduleIndex() {
|
|
4
|
+
return (
|
|
5
|
+
<PlaceholderPage
|
|
6
|
+
title="Schedule"
|
|
7
|
+
description="Scheduled tasks and cron jobs 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="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
11
|
+
</svg>
|
|
12
|
+
}
|
|
13
|
+
/>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { useNavigate, useSearchParams } from 'react-router-dom'
|
|
3
|
+
import { getEntries } 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 ViewsIndex() {
|
|
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
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
loadEntries()
|
|
23
|
+
}, [page, tagFilter])
|
|
24
|
+
|
|
25
|
+
async function loadEntries() {
|
|
26
|
+
setLoading(true)
|
|
27
|
+
try {
|
|
28
|
+
const response = await getEntries({
|
|
29
|
+
type: 'view',
|
|
30
|
+
tag: tagFilter || undefined,
|
|
31
|
+
page
|
|
32
|
+
})
|
|
33
|
+
setEntries(response.data)
|
|
34
|
+
setTotalPages(response.meta.total_pages)
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error('Failed to load entries:', error)
|
|
37
|
+
} finally {
|
|
38
|
+
setLoading(false)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function handleSearch(value: string) {
|
|
43
|
+
setPage(1)
|
|
44
|
+
if (value) {
|
|
45
|
+
setSearchParams({ tag: value })
|
|
46
|
+
} else {
|
|
47
|
+
setSearchParams({})
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function handlePageChange(newPage: number) {
|
|
52
|
+
setPage(newPage)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getViewTypeBadge(viewType: string) {
|
|
56
|
+
const colors: Record<string, string> = {
|
|
57
|
+
template: 'bg-blue-500/20 text-blue-400',
|
|
58
|
+
partial: 'bg-purple-500/20 text-purple-400',
|
|
59
|
+
layout: 'bg-green-500/20 text-green-400'
|
|
60
|
+
}
|
|
61
|
+
return (
|
|
62
|
+
<span className={`px-2 py-0.5 rounded text-xs font-medium ${colors[viewType] || 'bg-gray-500/20 text-gray-400'}`}>
|
|
63
|
+
{viewType}
|
|
64
|
+
</span>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getDurationColor(duration: number) {
|
|
69
|
+
if (duration > 100) return 'text-red-400'
|
|
70
|
+
if (duration > 50) return 'text-yellow-400'
|
|
71
|
+
return 'text-dark-muted'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div className="p-6">
|
|
76
|
+
<div className="mb-6">
|
|
77
|
+
<h1 className="text-2xl font-semibold text-white">Views</h1>
|
|
78
|
+
<p className="text-dark-muted text-sm mt-1">View templates rendered by ActionView</p>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div className="mb-4">
|
|
82
|
+
<SearchInput
|
|
83
|
+
placeholder="Search by tag..."
|
|
84
|
+
value={tagFilter}
|
|
85
|
+
onChange={handleSearch}
|
|
86
|
+
className="max-w-sm"
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<Card>
|
|
91
|
+
<Table>
|
|
92
|
+
<TableHeader>
|
|
93
|
+
<TableRow>
|
|
94
|
+
<TableHead>Type</TableHead>
|
|
95
|
+
<TableHead>Name</TableHead>
|
|
96
|
+
<TableHead>Path</TableHead>
|
|
97
|
+
<TableHead className="text-right">Duration</TableHead>
|
|
98
|
+
<TableHead>Happened</TableHead>
|
|
99
|
+
</TableRow>
|
|
100
|
+
</TableHeader>
|
|
101
|
+
<TableBody>
|
|
102
|
+
{loading ? (
|
|
103
|
+
<TableRow>
|
|
104
|
+
<TableCell className="text-center text-dark-muted py-8" colSpan={5}>
|
|
105
|
+
Loading...
|
|
106
|
+
</TableCell>
|
|
107
|
+
</TableRow>
|
|
108
|
+
) : entries.length === 0 ? (
|
|
109
|
+
<TableRow>
|
|
110
|
+
<TableCell className="text-center text-dark-muted py-8" colSpan={5}>
|
|
111
|
+
{tagFilter ? `No views found with tag "${tagFilter}".` : 'No views recorded yet.'}
|
|
112
|
+
</TableCell>
|
|
113
|
+
</TableRow>
|
|
114
|
+
) : (
|
|
115
|
+
entries.map((entry) => (
|
|
116
|
+
<TableRow key={entry.id} onClick={() => navigate(`/views/${entry.id}`)}>
|
|
117
|
+
<TableCell>
|
|
118
|
+
{getViewTypeBadge(String(entry.payload.view_type))}
|
|
119
|
+
</TableCell>
|
|
120
|
+
<TableCell className="font-mono text-sm text-white" title={String(entry.payload.name)}>
|
|
121
|
+
{truncate(String(entry.payload.name), 40)}
|
|
122
|
+
</TableCell>
|
|
123
|
+
<TableCell className="font-mono text-sm text-dark-muted" title={String(entry.payload.path)}>
|
|
124
|
+
{truncate(String(entry.payload.path), 40)}
|
|
125
|
+
</TableCell>
|
|
126
|
+
<TableCell className={`text-right ${getDurationColor(Number(entry.payload.duration))}`}>
|
|
127
|
+
{String(entry.payload.duration)}ms
|
|
128
|
+
</TableCell>
|
|
129
|
+
<TableCell className="text-dark-muted" title={entry.occurred_at}>
|
|
130
|
+
{timeAgo(entry.occurred_at)}
|
|
131
|
+
</TableCell>
|
|
132
|
+
</TableRow>
|
|
133
|
+
))
|
|
134
|
+
)}
|
|
135
|
+
</TableBody>
|
|
136
|
+
</Table>
|
|
137
|
+
<Pagination currentPage={page} totalPages={totalPages} onPageChange={handlePageChange} />
|
|
138
|
+
</Card>
|
|
139
|
+
</div>
|
|
140
|
+
)
|
|
141
|
+
}
|