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
data/client/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "railscope-client",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc && vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"react": "^18.2.0",
|
|
13
|
+
"react-dom": "^18.2.0",
|
|
14
|
+
"react-router-dom": "^6.20.0",
|
|
15
|
+
"clsx": "^2.0.0",
|
|
16
|
+
"tailwind-merge": "^2.1.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/react": "^18.2.0",
|
|
20
|
+
"@types/react-dom": "^18.2.0",
|
|
21
|
+
"@vitejs/plugin-react": "^4.2.0",
|
|
22
|
+
"autoprefixer": "^10.4.16",
|
|
23
|
+
"postcss": "^8.4.32",
|
|
24
|
+
"tailwindcss": "^3.3.6",
|
|
25
|
+
"typescript": "^5.3.0",
|
|
26
|
+
"vite": "^5.0.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
data/client/src/App.tsx
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Routes, Route } from 'react-router-dom'
|
|
2
|
+
import Layout from './components/Layout'
|
|
3
|
+
import RequestsIndex from './screens/requests/Index'
|
|
4
|
+
import RequestsShow from './screens/requests/Show'
|
|
5
|
+
import CommandsIndex from './screens/commands/Index'
|
|
6
|
+
import CommandsShow from './screens/commands/Show'
|
|
7
|
+
import ScheduleIndex from './screens/schedule/Index'
|
|
8
|
+
import JobsIndex from './screens/jobs/Index'
|
|
9
|
+
import JobsShow from './screens/jobs/Show'
|
|
10
|
+
import ExceptionsIndex from './screens/exceptions/Index'
|
|
11
|
+
import ExceptionsShow from './screens/exceptions/Show'
|
|
12
|
+
import LogsIndex from './screens/logs/Index'
|
|
13
|
+
import DumpsIndex from './screens/dumps/Index'
|
|
14
|
+
import QueriesIndex from './screens/queries/Index'
|
|
15
|
+
import QueriesShow from './screens/queries/Show'
|
|
16
|
+
import ModelsIndex from './screens/models/Index'
|
|
17
|
+
import EventsIndex from './screens/events/Index'
|
|
18
|
+
import MailIndex from './screens/mail/Index'
|
|
19
|
+
import NotificationsIndex from './screens/notifications/Index'
|
|
20
|
+
import GatesIndex from './screens/gates/Index'
|
|
21
|
+
import CacheIndex from './screens/cache/Index'
|
|
22
|
+
import RedisIndex from './screens/redis/Index'
|
|
23
|
+
import ViewsIndex from './screens/views/Index'
|
|
24
|
+
import ViewsShow from './screens/views/Show'
|
|
25
|
+
import ClientRequestsIndex from './screens/client-requests/Index'
|
|
26
|
+
|
|
27
|
+
function App() {
|
|
28
|
+
return (
|
|
29
|
+
<Layout>
|
|
30
|
+
<Routes>
|
|
31
|
+
<Route path="/" element={<RequestsIndex />} />
|
|
32
|
+
<Route path="/requests" element={<RequestsIndex />} />
|
|
33
|
+
<Route path="/requests/:id" element={<RequestsShow />} />
|
|
34
|
+
<Route path="/commands" element={<CommandsIndex />} />
|
|
35
|
+
<Route path="/commands/:id" element={<CommandsShow />} />
|
|
36
|
+
<Route path="/schedule" element={<ScheduleIndex />} />
|
|
37
|
+
<Route path="/jobs" element={<JobsIndex />} />
|
|
38
|
+
<Route path="/jobs/:id" element={<JobsShow />} />
|
|
39
|
+
<Route path="/exceptions" element={<ExceptionsIndex />} />
|
|
40
|
+
<Route path="/exceptions/:id" element={<ExceptionsShow />} />
|
|
41
|
+
<Route path="/logs" element={<LogsIndex />} />
|
|
42
|
+
<Route path="/dumps" element={<DumpsIndex />} />
|
|
43
|
+
<Route path="/queries" element={<QueriesIndex />} />
|
|
44
|
+
<Route path="/queries/:id" element={<QueriesShow />} />
|
|
45
|
+
<Route path="/models" element={<ModelsIndex />} />
|
|
46
|
+
<Route path="/events" element={<EventsIndex />} />
|
|
47
|
+
<Route path="/mail" element={<MailIndex />} />
|
|
48
|
+
<Route path="/notifications" element={<NotificationsIndex />} />
|
|
49
|
+
<Route path="/gates" element={<GatesIndex />} />
|
|
50
|
+
<Route path="/cache" element={<CacheIndex />} />
|
|
51
|
+
<Route path="/redis" element={<RedisIndex />} />
|
|
52
|
+
<Route path="/views" element={<ViewsIndex />} />
|
|
53
|
+
<Route path="/views/:id" element={<ViewsShow />} />
|
|
54
|
+
<Route path="/client-requests" element={<ClientRequestsIndex />} />
|
|
55
|
+
</Routes>
|
|
56
|
+
</Layout>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default App
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const API_BASE = '/railscope/api'
|
|
2
|
+
|
|
3
|
+
async function request<T>(endpoint: string, options?: RequestInit): Promise<T> {
|
|
4
|
+
const response = await fetch(`${API_BASE}${endpoint}`, {
|
|
5
|
+
headers: {
|
|
6
|
+
'Content-Type': 'application/json',
|
|
7
|
+
'Accept': 'application/json',
|
|
8
|
+
},
|
|
9
|
+
...options,
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
if (!response.ok) {
|
|
13
|
+
throw new Error(`API Error: ${response.status}`)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return response.json()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const api = {
|
|
20
|
+
get: <T>(endpoint: string) => request<T>(endpoint),
|
|
21
|
+
post: <T>(endpoint: string, data: unknown) =>
|
|
22
|
+
request<T>(endpoint, { method: 'POST', body: JSON.stringify(data) }),
|
|
23
|
+
delete: <T>(endpoint: string) =>
|
|
24
|
+
request<T>(endpoint, { method: 'DELETE' }),
|
|
25
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { api } from './client'
|
|
2
|
+
import type { Entry, PaginatedResponse, EntryResponse, BatchResponse, FamilyResponse } from '@/lib/types'
|
|
3
|
+
|
|
4
|
+
export interface EntriesParams {
|
|
5
|
+
type?: string
|
|
6
|
+
tag?: string
|
|
7
|
+
batch_id?: string
|
|
8
|
+
page?: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function getEntries(params: EntriesParams = {}): Promise<PaginatedResponse<Entry>> {
|
|
12
|
+
const searchParams = new URLSearchParams()
|
|
13
|
+
if (params.type) searchParams.set('type', params.type)
|
|
14
|
+
if (params.tag) searchParams.set('tag', params.tag)
|
|
15
|
+
if (params.batch_id) searchParams.set('batch_id', params.batch_id)
|
|
16
|
+
if (params.page) searchParams.set('page', params.page.toString())
|
|
17
|
+
|
|
18
|
+
const query = searchParams.toString()
|
|
19
|
+
return api.get(`/entries${query ? `?${query}` : ''}`)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function getEntry(id: number | string): Promise<EntryResponse> {
|
|
23
|
+
return api.get(`/entries/${id}`)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function getBatchEntries(batchId: string): Promise<BatchResponse> {
|
|
27
|
+
return api.get(`/entries/batch/${batchId}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function getFamilyEntries(familyHash: string, page = 1): Promise<FamilyResponse> {
|
|
31
|
+
return api.get(`/entries/family/${familyHash}?page=${page}`)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function deleteAllEntries(): Promise<void> {
|
|
35
|
+
return api.delete('/entries')
|
|
36
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ReactNode } from 'react'
|
|
2
|
+
import Sidebar from './Sidebar'
|
|
3
|
+
|
|
4
|
+
interface LayoutProps {
|
|
5
|
+
children: ReactNode
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function Layout({ children }: LayoutProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div className="flex h-screen overflow-hidden">
|
|
11
|
+
<Sidebar />
|
|
12
|
+
<main className="flex-1 overflow-auto">
|
|
13
|
+
{children}
|
|
14
|
+
</main>
|
|
15
|
+
</div>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Card, CardContent } from '@/components/ui/Card'
|
|
2
|
+
|
|
3
|
+
interface PlaceholderPageProps {
|
|
4
|
+
title: string
|
|
5
|
+
description: string
|
|
6
|
+
icon: React.ReactNode
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function PlaceholderPage({ title, description, icon }: PlaceholderPageProps) {
|
|
10
|
+
return (
|
|
11
|
+
<div className="p-6">
|
|
12
|
+
<div className="mb-6">
|
|
13
|
+
<h1 className="text-2xl font-semibold text-white">{title}</h1>
|
|
14
|
+
<p className="text-dark-muted text-sm mt-1">{description}</p>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<Card>
|
|
18
|
+
<CardContent className="py-16">
|
|
19
|
+
<div className="flex flex-col items-center justify-center text-center">
|
|
20
|
+
<div className="w-16 h-16 rounded-full bg-dark-border/50 flex items-center justify-center mb-4 text-dark-muted">
|
|
21
|
+
{icon}
|
|
22
|
+
</div>
|
|
23
|
+
<h2 className="text-lg font-medium text-white mb-2">To Implement</h2>
|
|
24
|
+
<p className="text-dark-muted text-sm max-w-md">
|
|
25
|
+
This feature is planned but not yet implemented. It will capture and display {title.toLowerCase()} from your Rails application.
|
|
26
|
+
</p>
|
|
27
|
+
</div>
|
|
28
|
+
</CardContent>
|
|
29
|
+
</Card>
|
|
30
|
+
</div>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { NavLink } from 'react-router-dom'
|
|
2
|
+
import { cn } from '@/lib/utils'
|
|
3
|
+
|
|
4
|
+
const navigation = [
|
|
5
|
+
{ name: 'Requests', href: '/requests', icon: RequestIcon },
|
|
6
|
+
{ name: 'Commands', href: '/commands', icon: CommandIcon },
|
|
7
|
+
{ name: 'Schedule', href: '/schedule', icon: ScheduleIcon },
|
|
8
|
+
{ name: 'Jobs', href: '/jobs', icon: JobIcon },
|
|
9
|
+
{ name: 'Exceptions', href: '/exceptions', icon: ExceptionIcon },
|
|
10
|
+
{ name: 'Logs', href: '/logs', icon: LogIcon },
|
|
11
|
+
{ name: 'Dumps', href: '/dumps', icon: DumpIcon },
|
|
12
|
+
{ name: 'Queries', href: '/queries', icon: QueryIcon },
|
|
13
|
+
{ name: 'Models', href: '/models', icon: ModelIcon },
|
|
14
|
+
{ name: 'Events', href: '/events', icon: EventIcon },
|
|
15
|
+
{ name: 'Mail', href: '/mail', icon: MailIcon },
|
|
16
|
+
{ name: 'Notifications', href: '/notifications', icon: NotificationIcon },
|
|
17
|
+
{ name: 'Gates', href: '/gates', icon: GateIcon },
|
|
18
|
+
{ name: 'Cache', href: '/cache', icon: CacheIcon },
|
|
19
|
+
{ name: 'Redis', href: '/redis', icon: RedisIcon },
|
|
20
|
+
{ name: 'Views', href: '/views', icon: ViewIcon },
|
|
21
|
+
{ name: 'HTTP Client', href: '/client-requests', icon: ClientRequestIcon },
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
export default function Sidebar() {
|
|
25
|
+
return (
|
|
26
|
+
<aside className="w-56 bg-dark-surface border-r border-dark-border flex flex-col">
|
|
27
|
+
<div className="p-4 border-b border-dark-border">
|
|
28
|
+
<h1 className="text-lg font-semibold text-white flex items-center gap-2">
|
|
29
|
+
<svg className="w-6 h-6 text-blue-500" fill="currentColor" viewBox="0 0 24 24">
|
|
30
|
+
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/>
|
|
31
|
+
</svg>
|
|
32
|
+
Railscope
|
|
33
|
+
</h1>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<nav className="flex-1 p-2 overflow-y-auto">
|
|
37
|
+
{navigation.map((item) => (
|
|
38
|
+
<NavLink
|
|
39
|
+
key={item.name}
|
|
40
|
+
to={item.href}
|
|
41
|
+
className={({ isActive }) =>
|
|
42
|
+
cn(
|
|
43
|
+
'flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors',
|
|
44
|
+
isActive
|
|
45
|
+
? 'bg-blue-500/10 text-blue-400'
|
|
46
|
+
: 'text-dark-muted hover:text-dark-text hover:bg-dark-border/50'
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
>
|
|
50
|
+
<item.icon className="w-5 h-5" />
|
|
51
|
+
{item.name}
|
|
52
|
+
</NavLink>
|
|
53
|
+
))}
|
|
54
|
+
</nav>
|
|
55
|
+
|
|
56
|
+
<div className="p-4 border-t border-dark-border text-xs text-dark-muted">
|
|
57
|
+
Railscope v0.1.0
|
|
58
|
+
</div>
|
|
59
|
+
</aside>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function RequestIcon({ className }: { className?: string }) {
|
|
64
|
+
return (
|
|
65
|
+
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
66
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
67
|
+
</svg>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function CommandIcon({ className }: { className?: string }) {
|
|
72
|
+
return (
|
|
73
|
+
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
74
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
75
|
+
</svg>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function ScheduleIcon({ className }: { className?: string }) {
|
|
80
|
+
return (
|
|
81
|
+
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
82
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
83
|
+
</svg>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function JobIcon({ className }: { className?: string }) {
|
|
88
|
+
return (
|
|
89
|
+
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
90
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
|
91
|
+
</svg>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function ExceptionIcon({ className }: { className?: string }) {
|
|
96
|
+
return (
|
|
97
|
+
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
98
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
99
|
+
</svg>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function LogIcon({ className }: { className?: string }) {
|
|
104
|
+
return (
|
|
105
|
+
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
106
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
107
|
+
</svg>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function DumpIcon({ className }: { className?: string }) {
|
|
112
|
+
return (
|
|
113
|
+
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
114
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} 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" />
|
|
115
|
+
</svg>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function QueryIcon({ className }: { className?: string }) {
|
|
120
|
+
return (
|
|
121
|
+
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
122
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4" />
|
|
123
|
+
</svg>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function ModelIcon({ className }: { className?: string }) {
|
|
128
|
+
return (
|
|
129
|
+
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
130
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
|
131
|
+
</svg>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function EventIcon({ className }: { className?: string }) {
|
|
136
|
+
return (
|
|
137
|
+
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
138
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
139
|
+
</svg>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function MailIcon({ className }: { className?: string }) {
|
|
144
|
+
return (
|
|
145
|
+
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
146
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
|
147
|
+
</svg>
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function NotificationIcon({ className }: { className?: string }) {
|
|
152
|
+
return (
|
|
153
|
+
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
154
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
|
|
155
|
+
</svg>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function GateIcon({ className }: { className?: string }) {
|
|
160
|
+
return (
|
|
161
|
+
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
162
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
|
163
|
+
</svg>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function CacheIcon({ className }: { className?: string }) {
|
|
168
|
+
return (
|
|
169
|
+
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
170
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
|
|
171
|
+
</svg>
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function RedisIcon({ className }: { className?: string }) {
|
|
176
|
+
return (
|
|
177
|
+
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
178
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
|
|
179
|
+
</svg>
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function ViewIcon({ className }: { className?: string }) {
|
|
184
|
+
return (
|
|
185
|
+
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
186
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
187
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
188
|
+
</svg>
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function ClientRequestIcon({ className }: { className?: string }) {
|
|
193
|
+
return (
|
|
194
|
+
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
195
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} 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" />
|
|
196
|
+
</svg>
|
|
197
|
+
)
|
|
198
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils'
|
|
2
|
+
|
|
3
|
+
type BadgeVariant = 'default' | 'success' | 'warning' | 'error' | 'info' | 'purple'
|
|
4
|
+
|
|
5
|
+
interface BadgeProps {
|
|
6
|
+
children: React.ReactNode
|
|
7
|
+
variant?: BadgeVariant
|
|
8
|
+
className?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const variants: Record<BadgeVariant, string> = {
|
|
12
|
+
default: 'bg-dark-border text-dark-muted',
|
|
13
|
+
success: 'bg-green-500/20 text-green-400',
|
|
14
|
+
warning: 'bg-yellow-500/20 text-yellow-400',
|
|
15
|
+
error: 'bg-red-500/20 text-red-400',
|
|
16
|
+
info: 'bg-blue-500/20 text-blue-400',
|
|
17
|
+
purple: 'bg-purple-500/20 text-purple-400',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function Badge({ children, variant = 'default', className }: BadgeProps) {
|
|
21
|
+
return (
|
|
22
|
+
<span
|
|
23
|
+
className={cn(
|
|
24
|
+
'inline-flex items-center px-2 py-0.5 rounded text-xs font-medium',
|
|
25
|
+
variants[variant],
|
|
26
|
+
className
|
|
27
|
+
)}
|
|
28
|
+
>
|
|
29
|
+
{children}
|
|
30
|
+
</span>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function MethodBadge({ method }: { method: string }) {
|
|
35
|
+
const variantMap: Record<string, BadgeVariant> = {
|
|
36
|
+
GET: 'info',
|
|
37
|
+
POST: 'success',
|
|
38
|
+
PUT: 'warning',
|
|
39
|
+
PATCH: 'warning',
|
|
40
|
+
DELETE: 'error',
|
|
41
|
+
}
|
|
42
|
+
const variant = variantMap[method] || 'default'
|
|
43
|
+
|
|
44
|
+
return <Badge variant={variant}>{method}</Badge>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function StatusBadge({ status }: { status: number }) {
|
|
48
|
+
let variant: BadgeVariant = 'default'
|
|
49
|
+
if (status >= 200 && status < 300) variant = 'success'
|
|
50
|
+
else if (status >= 300 && status < 400) variant = 'warning'
|
|
51
|
+
else if (status >= 400) variant = 'error'
|
|
52
|
+
|
|
53
|
+
return <Badge variant={variant}>{status}</Badge>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function TypeBadge({ type }: { type: string }) {
|
|
57
|
+
const variantMap: Record<string, BadgeVariant> = {
|
|
58
|
+
request: 'info',
|
|
59
|
+
query: 'purple',
|
|
60
|
+
exception: 'error',
|
|
61
|
+
job_enqueue: 'success',
|
|
62
|
+
job_perform: 'success',
|
|
63
|
+
}
|
|
64
|
+
const variant = variantMap[type] || 'default'
|
|
65
|
+
|
|
66
|
+
return <Badge variant={variant}>{type}</Badge>
|
|
67
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils'
|
|
2
|
+
|
|
3
|
+
interface CardProps {
|
|
4
|
+
children: React.ReactNode
|
|
5
|
+
className?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Card({ children, className }: CardProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div className={cn('bg-dark-surface border border-dark-border rounded-lg', className)}>
|
|
11
|
+
{children}
|
|
12
|
+
</div>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function CardHeader({ children, className }: CardProps) {
|
|
17
|
+
return (
|
|
18
|
+
<div className={cn('px-4 py-3 border-b border-dark-border', className)}>
|
|
19
|
+
{children}
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function CardTitle({ children, className }: CardProps) {
|
|
25
|
+
return (
|
|
26
|
+
<h3 className={cn('text-sm font-semibold text-dark-text uppercase tracking-wide', className)}>
|
|
27
|
+
{children}
|
|
28
|
+
</h3>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function CardContent({ children, className }: CardProps) {
|
|
33
|
+
return (
|
|
34
|
+
<div className={cn('p-4', className)}>
|
|
35
|
+
{children}
|
|
36
|
+
</div>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils'
|
|
2
|
+
|
|
3
|
+
interface JsonViewerProps {
|
|
4
|
+
data: unknown
|
|
5
|
+
className?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function JsonViewer({ data, className }: JsonViewerProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div className={cn('font-mono text-sm bg-black/20 rounded-md p-4 overflow-auto', className)}>
|
|
11
|
+
<JsonValue value={data} />
|
|
12
|
+
</div>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function JsonValue({ value, indent = 0 }: { value: unknown; indent?: number }) {
|
|
17
|
+
if (value === null) {
|
|
18
|
+
return <span className="text-dark-muted italic">null</span>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (typeof value === 'boolean') {
|
|
22
|
+
return <span className="text-purple-400">{value.toString()}</span>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (typeof value === 'number') {
|
|
26
|
+
return <span className="text-yellow-400">{value}</span>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (typeof value === 'string') {
|
|
30
|
+
if (value.length > 100) {
|
|
31
|
+
return (
|
|
32
|
+
<span className="text-green-400 block whitespace-pre-wrap break-all">
|
|
33
|
+
"{value}"
|
|
34
|
+
</span>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
return <span className="text-green-400">"{value}"</span>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (Array.isArray(value)) {
|
|
41
|
+
if (value.length === 0) {
|
|
42
|
+
return <span className="text-dark-muted">[]</span>
|
|
43
|
+
}
|
|
44
|
+
return (
|
|
45
|
+
<div>
|
|
46
|
+
<span className="text-dark-muted">[</span>
|
|
47
|
+
<div className="pl-4">
|
|
48
|
+
{value.map((item, i) => (
|
|
49
|
+
<div key={i}>
|
|
50
|
+
<JsonValue value={item} indent={indent + 1} />
|
|
51
|
+
{i < value.length - 1 && <span className="text-dark-muted">,</span>}
|
|
52
|
+
</div>
|
|
53
|
+
))}
|
|
54
|
+
</div>
|
|
55
|
+
<span className="text-dark-muted">]</span>
|
|
56
|
+
</div>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (typeof value === 'object') {
|
|
61
|
+
const entries = Object.entries(value as Record<string, unknown>)
|
|
62
|
+
if (entries.length === 0) {
|
|
63
|
+
return <span className="text-dark-muted">{'{}'}</span>
|
|
64
|
+
}
|
|
65
|
+
return (
|
|
66
|
+
<div>
|
|
67
|
+
{entries.map(([key, val], i) => (
|
|
68
|
+
<div key={key} className="flex">
|
|
69
|
+
<span className="text-blue-400">"{key}"</span>
|
|
70
|
+
<span className="text-dark-muted mr-2">:</span>
|
|
71
|
+
<JsonValue value={val} indent={indent + 1} />
|
|
72
|
+
{i < entries.length - 1 && <span className="text-dark-muted">,</span>}
|
|
73
|
+
</div>
|
|
74
|
+
))}
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return <span className="text-dark-text">{String(value)}</span>
|
|
80
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils'
|
|
2
|
+
|
|
3
|
+
interface PaginationProps {
|
|
4
|
+
currentPage: number
|
|
5
|
+
totalPages: number
|
|
6
|
+
onPageChange: (page: number) => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function Pagination({ currentPage, totalPages, onPageChange }: PaginationProps) {
|
|
10
|
+
if (totalPages <= 1) return null
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div className="flex items-center justify-center gap-4 py-4">
|
|
14
|
+
<button
|
|
15
|
+
onClick={() => onPageChange(currentPage - 1)}
|
|
16
|
+
disabled={currentPage <= 1}
|
|
17
|
+
className={cn(
|
|
18
|
+
'px-3 py-1.5 text-sm rounded-md border border-dark-border',
|
|
19
|
+
currentPage <= 1
|
|
20
|
+
? 'text-dark-muted cursor-not-allowed'
|
|
21
|
+
: 'text-dark-text hover:bg-dark-border'
|
|
22
|
+
)}
|
|
23
|
+
>
|
|
24
|
+
Previous
|
|
25
|
+
</button>
|
|
26
|
+
|
|
27
|
+
<span className="text-sm text-dark-muted">
|
|
28
|
+
Page {currentPage} of {totalPages}
|
|
29
|
+
</span>
|
|
30
|
+
|
|
31
|
+
<button
|
|
32
|
+
onClick={() => onPageChange(currentPage + 1)}
|
|
33
|
+
disabled={currentPage >= totalPages}
|
|
34
|
+
className={cn(
|
|
35
|
+
'px-3 py-1.5 text-sm rounded-md border border-dark-border',
|
|
36
|
+
currentPage >= totalPages
|
|
37
|
+
? 'text-dark-muted cursor-not-allowed'
|
|
38
|
+
: 'text-dark-text hover:bg-dark-border'
|
|
39
|
+
)}
|
|
40
|
+
>
|
|
41
|
+
Next
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
}
|