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,70 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react'
|
|
2
|
+
import { useDebouncedCallback } from '@/lib/hooks'
|
|
3
|
+
|
|
4
|
+
interface SearchInputProps {
|
|
5
|
+
placeholder?: string
|
|
6
|
+
value?: string
|
|
7
|
+
onChange: (value: string) => void
|
|
8
|
+
className?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function SearchInput({
|
|
12
|
+
placeholder = 'Search...',
|
|
13
|
+
value = '',
|
|
14
|
+
onChange,
|
|
15
|
+
className = ''
|
|
16
|
+
}: SearchInputProps) {
|
|
17
|
+
const [inputValue, setInputValue] = useState(value)
|
|
18
|
+
|
|
19
|
+
const debouncedOnChange = useDebouncedCallback((val: string) => {
|
|
20
|
+
onChange(val)
|
|
21
|
+
}, 300)
|
|
22
|
+
|
|
23
|
+
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
24
|
+
const newValue = e.target.value
|
|
25
|
+
setInputValue(newValue)
|
|
26
|
+
debouncedOnChange(newValue)
|
|
27
|
+
}, [debouncedOnChange])
|
|
28
|
+
|
|
29
|
+
const handleClear = useCallback(() => {
|
|
30
|
+
setInputValue('')
|
|
31
|
+
onChange('')
|
|
32
|
+
}, [onChange])
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className={`relative ${className}`}>
|
|
36
|
+
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
37
|
+
<svg
|
|
38
|
+
className="h-4 w-4 text-dark-muted"
|
|
39
|
+
fill="none"
|
|
40
|
+
viewBox="0 0 24 24"
|
|
41
|
+
stroke="currentColor"
|
|
42
|
+
>
|
|
43
|
+
<path
|
|
44
|
+
strokeLinecap="round"
|
|
45
|
+
strokeLinejoin="round"
|
|
46
|
+
strokeWidth={2}
|
|
47
|
+
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
48
|
+
/>
|
|
49
|
+
</svg>
|
|
50
|
+
</div>
|
|
51
|
+
<input
|
|
52
|
+
type="text"
|
|
53
|
+
value={inputValue}
|
|
54
|
+
onChange={handleChange}
|
|
55
|
+
placeholder={placeholder}
|
|
56
|
+
className="w-full pl-10 pr-10 py-2 bg-dark-surface border border-dark-border rounded-md text-sm text-dark-text placeholder-dark-muted focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
|
|
57
|
+
/>
|
|
58
|
+
{inputValue && (
|
|
59
|
+
<button
|
|
60
|
+
onClick={handleClear}
|
|
61
|
+
className="absolute inset-y-0 right-0 pr-3 flex items-center text-dark-muted hover:text-dark-text"
|
|
62
|
+
>
|
|
63
|
+
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
64
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
65
|
+
</svg>
|
|
66
|
+
</button>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils'
|
|
2
|
+
|
|
3
|
+
interface TableProps {
|
|
4
|
+
children: React.ReactNode
|
|
5
|
+
className?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface TableCellProps extends TableProps {
|
|
9
|
+
colSpan?: number
|
|
10
|
+
title?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function Table({ children, className }: TableProps) {
|
|
14
|
+
return (
|
|
15
|
+
<div className={cn('overflow-x-auto', className)}>
|
|
16
|
+
<table className="w-full">
|
|
17
|
+
{children}
|
|
18
|
+
</table>
|
|
19
|
+
</div>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function TableHeader({ children, className }: TableProps) {
|
|
24
|
+
return (
|
|
25
|
+
<thead className={cn('bg-black/20', className)}>
|
|
26
|
+
{children}
|
|
27
|
+
</thead>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function TableBody({ children, className }: TableProps) {
|
|
32
|
+
return (
|
|
33
|
+
<tbody className={cn('divide-y divide-dark-border', className)}>
|
|
34
|
+
{children}
|
|
35
|
+
</tbody>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function TableRow({ children, className, onClick }: TableProps & { onClick?: () => void }) {
|
|
40
|
+
return (
|
|
41
|
+
<tr
|
|
42
|
+
className={cn(
|
|
43
|
+
'hover:bg-white/[0.02] transition-colors',
|
|
44
|
+
onClick && 'cursor-pointer',
|
|
45
|
+
className
|
|
46
|
+
)}
|
|
47
|
+
onClick={onClick}
|
|
48
|
+
>
|
|
49
|
+
{children}
|
|
50
|
+
</tr>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function TableHead({ children, className }: TableProps) {
|
|
55
|
+
return (
|
|
56
|
+
<th className={cn('px-4 py-3 text-left text-xs font-semibold text-dark-muted uppercase tracking-wide', className)}>
|
|
57
|
+
{children}
|
|
58
|
+
</th>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function TableCell({ children, className, colSpan, title }: TableCellProps) {
|
|
63
|
+
return (
|
|
64
|
+
<td className={cn('px-4 py-3 text-sm', className)} colSpan={colSpan} title={title}>
|
|
65
|
+
{children}
|
|
66
|
+
</td>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
body {
|
|
6
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
7
|
+
background-color: #0d1117;
|
|
8
|
+
color: #c9d1d9;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/* Scrollbar styling */
|
|
12
|
+
::-webkit-scrollbar {
|
|
13
|
+
width: 8px;
|
|
14
|
+
height: 8px;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
::-webkit-scrollbar-track {
|
|
18
|
+
background: #161b22;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
::-webkit-scrollbar-thumb {
|
|
22
|
+
background: #30363d;
|
|
23
|
+
border-radius: 4px;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
::-webkit-scrollbar-thumb:hover {
|
|
27
|
+
background: #484f58;
|
|
28
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useCallback, useRef, useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4
|
+
export function useDebouncedCallback<T extends (...args: any[]) => any>(
|
|
5
|
+
callback: T,
|
|
6
|
+
delay: number
|
|
7
|
+
): T {
|
|
8
|
+
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
9
|
+
const callbackRef = useRef(callback)
|
|
10
|
+
|
|
11
|
+
// Update callback ref when callback changes
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
callbackRef.current = callback
|
|
14
|
+
}, [callback])
|
|
15
|
+
|
|
16
|
+
// Cleanup on unmount
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
return () => {
|
|
19
|
+
if (timeoutRef.current) {
|
|
20
|
+
clearTimeout(timeoutRef.current)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}, [])
|
|
24
|
+
|
|
25
|
+
return useCallback(
|
|
26
|
+
((...args: Parameters<T>) => {
|
|
27
|
+
if (timeoutRef.current) {
|
|
28
|
+
clearTimeout(timeoutRef.current)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
timeoutRef.current = setTimeout(() => {
|
|
32
|
+
callbackRef.current(...args)
|
|
33
|
+
}, delay)
|
|
34
|
+
}) as T,
|
|
35
|
+
[delay]
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export interface Entry {
|
|
2
|
+
id: number
|
|
3
|
+
uuid: string
|
|
4
|
+
batch_id: string | null
|
|
5
|
+
family_hash: string | null
|
|
6
|
+
entry_type: string
|
|
7
|
+
payload: Record<string, unknown>
|
|
8
|
+
tags: string[]
|
|
9
|
+
occurred_at: string
|
|
10
|
+
created_at: string
|
|
11
|
+
family_count?: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface PaginatedResponse<T> {
|
|
15
|
+
data: T[]
|
|
16
|
+
meta: {
|
|
17
|
+
current_page: number
|
|
18
|
+
total_pages: number
|
|
19
|
+
total_count: number
|
|
20
|
+
per_page: number
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface EntryResponse {
|
|
25
|
+
data: Entry
|
|
26
|
+
batch: Entry[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface BatchResponse {
|
|
30
|
+
data: Entry[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface FamilyResponse {
|
|
34
|
+
data: Entry[]
|
|
35
|
+
meta: {
|
|
36
|
+
current_page: number
|
|
37
|
+
total_pages: number
|
|
38
|
+
total_count: number
|
|
39
|
+
per_page: number
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type EntryType =
|
|
44
|
+
| 'request'
|
|
45
|
+
| 'query'
|
|
46
|
+
| 'exception'
|
|
47
|
+
| 'job_enqueue'
|
|
48
|
+
| 'job_perform'
|
|
49
|
+
| 'command'
|
|
50
|
+
| 'schedule'
|
|
51
|
+
| 'log'
|
|
52
|
+
| 'dump'
|
|
53
|
+
| 'model'
|
|
54
|
+
| 'event'
|
|
55
|
+
| 'mail'
|
|
56
|
+
| 'notification'
|
|
57
|
+
| 'gate'
|
|
58
|
+
| 'cache'
|
|
59
|
+
| 'redis'
|
|
60
|
+
| 'view'
|
|
61
|
+
| 'client_request'
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { clsx, type ClassValue } from 'clsx'
|
|
2
|
+
import { twMerge } from 'tailwind-merge'
|
|
3
|
+
import { Entry } from './types'
|
|
4
|
+
|
|
5
|
+
export function cn(...inputs: ClassValue[]) {
|
|
6
|
+
return twMerge(clsx(inputs))
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function groupEntriesByType(entries: Entry[]): Record<string, Entry[]> {
|
|
10
|
+
return entries.reduce((acc, entry) => {
|
|
11
|
+
const type = entry.entry_type
|
|
12
|
+
if (!acc[type]) {
|
|
13
|
+
acc[type] = []
|
|
14
|
+
}
|
|
15
|
+
acc[type].push(entry)
|
|
16
|
+
return acc
|
|
17
|
+
}, {} as Record<string, Entry[]>)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function timeAgo(date: string): string {
|
|
21
|
+
const seconds = Math.floor((new Date().getTime() - new Date(date).getTime()) / 1000)
|
|
22
|
+
|
|
23
|
+
if (seconds < 60) return `${seconds}s ago`
|
|
24
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`
|
|
25
|
+
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`
|
|
26
|
+
return `${Math.floor(seconds / 86400)}d ago`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function truncate(str: string, length: number): string {
|
|
30
|
+
if (str.length <= length) return str
|
|
31
|
+
return str.slice(0, length) + '...'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function formatDuration(ms: number): string {
|
|
35
|
+
if (ms < 1) return '<1ms'
|
|
36
|
+
if (ms < 1000) return `${ms.toFixed(0)}ms`
|
|
37
|
+
return `${(ms / 1000).toFixed(2)}s`
|
|
38
|
+
}
|
data/client/src/main.tsx
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import ReactDOM from 'react-dom/client'
|
|
3
|
+
import { BrowserRouter } from 'react-router-dom'
|
|
4
|
+
import App from './App'
|
|
5
|
+
import './index.css'
|
|
6
|
+
|
|
7
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
8
|
+
<React.StrictMode>
|
|
9
|
+
<BrowserRouter basename="/railscope">
|
|
10
|
+
<App />
|
|
11
|
+
</BrowserRouter>
|
|
12
|
+
</React.StrictMode>,
|
|
13
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import PlaceholderPage from '@/components/PlaceholderPage'
|
|
2
|
+
|
|
3
|
+
export default function CacheIndex() {
|
|
4
|
+
return (
|
|
5
|
+
<PlaceholderPage
|
|
6
|
+
title="Cache"
|
|
7
|
+
description="Rails.cache operations (read, write, delete, fetch)"
|
|
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="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
|
|
11
|
+
</svg>
|
|
12
|
+
}
|
|
13
|
+
/>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import PlaceholderPage from '@/components/PlaceholderPage'
|
|
2
|
+
|
|
3
|
+
export default function ClientRequestsIndex() {
|
|
4
|
+
return (
|
|
5
|
+
<PlaceholderPage
|
|
6
|
+
title="HTTP Client"
|
|
7
|
+
description="Outgoing HTTP requests made by 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="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
|
|
11
|
+
</svg>
|
|
12
|
+
}
|
|
13
|
+
/>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
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 } from '@/lib/utils'
|
|
6
|
+
import { Card } from '@/components/ui/Card'
|
|
7
|
+
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@/components/ui/Table'
|
|
8
|
+
import { Badge } from '@/components/ui/Badge'
|
|
9
|
+
import { Pagination } from '@/components/ui/Pagination'
|
|
10
|
+
import { SearchInput } from '@/components/ui/SearchInput'
|
|
11
|
+
|
|
12
|
+
export default function CommandsIndex() {
|
|
13
|
+
const navigate = useNavigate()
|
|
14
|
+
const [searchParams, setSearchParams] = useSearchParams()
|
|
15
|
+
const [entries, setEntries] = useState<Entry[]>([])
|
|
16
|
+
const [loading, setLoading] = useState(true)
|
|
17
|
+
const [page, setPage] = useState(1)
|
|
18
|
+
const [totalPages, setTotalPages] = useState(1)
|
|
19
|
+
|
|
20
|
+
const tagFilter = searchParams.get('tag') || ''
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
loadEntries()
|
|
24
|
+
}, [page, tagFilter])
|
|
25
|
+
|
|
26
|
+
async function loadEntries() {
|
|
27
|
+
setLoading(true)
|
|
28
|
+
try {
|
|
29
|
+
const response = await getEntries({
|
|
30
|
+
type: 'command',
|
|
31
|
+
tag: tagFilter || undefined,
|
|
32
|
+
page
|
|
33
|
+
})
|
|
34
|
+
setEntries(response.data)
|
|
35
|
+
setTotalPages(response.meta.total_pages)
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error('Failed to load entries:', error)
|
|
38
|
+
} finally {
|
|
39
|
+
setLoading(false)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function handleSearch(value: string) {
|
|
44
|
+
setPage(1)
|
|
45
|
+
if (value) {
|
|
46
|
+
setSearchParams({ tag: value })
|
|
47
|
+
} else {
|
|
48
|
+
setSearchParams({})
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function handlePageChange(newPage: number) {
|
|
53
|
+
setPage(newPage)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className="p-6">
|
|
58
|
+
<div className="mb-6">
|
|
59
|
+
<h1 className="text-2xl font-semibold text-white">Commands</h1>
|
|
60
|
+
<p className="text-dark-muted text-sm mt-1">Rake tasks executed in your application</p>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div className="mb-4">
|
|
64
|
+
<SearchInput
|
|
65
|
+
placeholder="Search by tag..."
|
|
66
|
+
value={tagFilter}
|
|
67
|
+
onChange={handleSearch}
|
|
68
|
+
className="max-w-sm"
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<Card>
|
|
73
|
+
<Table>
|
|
74
|
+
<TableHeader>
|
|
75
|
+
<TableRow>
|
|
76
|
+
<TableHead>Command</TableHead>
|
|
77
|
+
<TableHead>Exit Code</TableHead>
|
|
78
|
+
<TableHead className="text-right">Duration</TableHead>
|
|
79
|
+
<TableHead>Happened</TableHead>
|
|
80
|
+
</TableRow>
|
|
81
|
+
</TableHeader>
|
|
82
|
+
<TableBody>
|
|
83
|
+
{loading ? (
|
|
84
|
+
<TableRow>
|
|
85
|
+
<TableCell className="text-center text-dark-muted py-8" colSpan={4}>
|
|
86
|
+
Loading...
|
|
87
|
+
</TableCell>
|
|
88
|
+
</TableRow>
|
|
89
|
+
) : entries.length === 0 ? (
|
|
90
|
+
<TableRow>
|
|
91
|
+
<TableCell className="text-center text-dark-muted py-8" colSpan={4}>
|
|
92
|
+
{tagFilter ? `No commands found with tag "${tagFilter}".` : 'No commands recorded yet.'}
|
|
93
|
+
</TableCell>
|
|
94
|
+
</TableRow>
|
|
95
|
+
) : (
|
|
96
|
+
entries.map((entry) => {
|
|
97
|
+
const payload = entry.payload as Record<string, unknown>
|
|
98
|
+
const isFailed = entry.tags.includes('failed')
|
|
99
|
+
const exitCode = Number(payload.exit_code ?? 0)
|
|
100
|
+
return (
|
|
101
|
+
<TableRow key={entry.id} onClick={() => navigate(`/commands/${entry.id}`)}>
|
|
102
|
+
<TableCell>
|
|
103
|
+
<span className={`font-mono ${isFailed ? 'text-red-400' : 'text-green-400'}`}>
|
|
104
|
+
{String(payload.command)}
|
|
105
|
+
</span>
|
|
106
|
+
{payload.description ? (
|
|
107
|
+
<div className="text-dark-muted text-xs mt-0.5">
|
|
108
|
+
{String(payload.description)}
|
|
109
|
+
</div>
|
|
110
|
+
) : null}
|
|
111
|
+
</TableCell>
|
|
112
|
+
<TableCell>
|
|
113
|
+
<Badge variant={exitCode === 0 ? 'success' : 'error'}>
|
|
114
|
+
{exitCode}
|
|
115
|
+
</Badge>
|
|
116
|
+
</TableCell>
|
|
117
|
+
<TableCell className="text-right text-dark-muted">
|
|
118
|
+
{payload.duration ? `${String(payload.duration)}ms` : '-'}
|
|
119
|
+
</TableCell>
|
|
120
|
+
<TableCell className="text-dark-muted" title={entry.occurred_at}>
|
|
121
|
+
{timeAgo(entry.occurred_at)}
|
|
122
|
+
</TableCell>
|
|
123
|
+
</TableRow>
|
|
124
|
+
)
|
|
125
|
+
})
|
|
126
|
+
)}
|
|
127
|
+
</TableBody>
|
|
128
|
+
</Table>
|
|
129
|
+
<Pagination currentPage={page} totalPages={totalPages} onPageChange={handlePageChange} />
|
|
130
|
+
</Card>
|
|
131
|
+
</div>
|
|
132
|
+
)
|
|
133
|
+
}
|