@wakastellar/ui 2.1.2 → 2.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/blocks/apm-overview/index.d.ts +58 -0
- package/dist/blocks/cicd-builder/index.d.ts +47 -0
- package/dist/blocks/cloud-cost-dashboard/index.d.ts +49 -0
- package/dist/blocks/container-orchestrator/index.d.ts +63 -0
- package/dist/blocks/database-admin/index.d.ts +84 -0
- package/dist/blocks/gitops-sync-status/index.d.ts +45 -0
- package/dist/blocks/incident-manager/index.d.ts +44 -0
- package/dist/blocks/index.d.ts +10 -0
- package/dist/blocks/infrastructure-map/index.d.ts +32 -0
- package/dist/blocks/on-call-schedule/index.d.ts +43 -0
- package/dist/blocks/release-notes/index.d.ts +49 -0
- package/dist/components/index.d.ts +34 -0
- package/dist/components/waka-ad-banner/index.d.ts +36 -0
- package/dist/components/waka-ad-fallback/index.d.ts +33 -0
- package/dist/components/waka-ad-inline/index.d.ts +15 -0
- package/dist/components/waka-ad-interstitial/index.d.ts +26 -0
- package/dist/components/waka-ad-placeholder/index.d.ts +17 -0
- package/dist/components/waka-ad-provider/index.d.ts +103 -0
- package/dist/components/waka-ad-sidebar/index.d.ts +18 -0
- package/dist/components/waka-ad-sticky-footer/index.d.ts +17 -0
- package/dist/components/waka-alert-panel/index.d.ts +45 -0
- package/dist/components/waka-artifact-list/index.d.ts +32 -0
- package/dist/components/waka-build-matrix/index.d.ts +36 -0
- package/dist/components/waka-config-comparator/index.d.ts +37 -0
- package/dist/components/waka-container-list/index.d.ts +51 -0
- package/dist/components/waka-content-recommendation/index.d.ts +23 -0
- package/dist/components/waka-database-card/index.d.ts +46 -0
- package/dist/components/waka-dependency-tree/index.d.ts +38 -0
- package/dist/components/waka-env-var-editor/index.d.ts +30 -0
- package/dist/components/waka-feature-flag-row/index.d.ts +45 -0
- package/dist/components/waka-kubernetes-overview/index.d.ts +98 -0
- package/dist/components/waka-log-viewer/index.d.ts +38 -0
- package/dist/components/waka-migration-list/index.d.ts +36 -0
- package/dist/components/waka-outstream-video/index.d.ts +24 -0
- package/dist/components/waka-pod-card/index.d.ts +73 -0
- package/dist/components/waka-query-explain/index.d.ts +48 -0
- package/dist/components/waka-secret-card/index.d.ts +43 -0
- package/dist/components/waka-security-scan-result/index.d.ts +45 -0
- package/dist/components/waka-service-graph/index.d.ts +44 -0
- package/dist/components/waka-sponsored-badge/index.d.ts +20 -0
- package/dist/components/waka-sponsored-card/index.d.ts +25 -0
- package/dist/components/waka-sponsored-feed/index.d.ts +31 -0
- package/dist/components/waka-test-report/index.d.ts +60 -0
- package/dist/components/waka-trace-viewer/index.d.ts +36 -0
- package/dist/components/waka-video-ad/index.d.ts +32 -0
- package/dist/components/waka-video-overlay/index.d.ts +26 -0
- package/dist/index.cjs.js +251 -200
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +47315 -35823
- package/dist/utils/security.d.ts +96 -0
- package/package.json +4 -4
- package/src/blocks/apm-overview/index.tsx +672 -0
- package/src/blocks/cicd-builder/index.tsx +738 -0
- package/src/blocks/cloud-cost-dashboard/index.tsx +597 -0
- package/src/blocks/container-orchestrator/index.tsx +729 -0
- package/src/blocks/database-admin/index.tsx +679 -0
- package/src/blocks/gitops-sync-status/index.tsx +557 -0
- package/src/blocks/incident-manager/index.tsx +586 -0
- package/src/blocks/index.ts +119 -0
- package/src/blocks/infrastructure-map/index.tsx +638 -0
- package/src/blocks/on-call-schedule/index.tsx +615 -0
- package/src/blocks/release-notes/index.tsx +643 -0
- package/src/blocks/sidebar/index.tsx +6 -6
- package/src/components/DataTable/templates/index.tsx +3 -2
- package/src/components/index.ts +283 -0
- package/src/components/waka-3d-pie-chart/index.tsx +11 -11
- package/src/components/waka-achievement-unlock/index.tsx +16 -16
- package/src/components/waka-ad-banner/index.tsx +275 -0
- package/src/components/waka-ad-fallback/index.tsx +181 -0
- package/src/components/waka-ad-inline/index.tsx +103 -0
- package/src/components/waka-ad-interstitial/index.tsx +278 -0
- package/src/components/waka-ad-placeholder/index.tsx +84 -0
- package/src/components/waka-ad-provider/index.tsx +329 -0
- package/src/components/waka-ad-sidebar/index.tsx +113 -0
- package/src/components/waka-ad-sticky-footer/index.tsx +125 -0
- package/src/components/waka-alert-panel/index.tsx +493 -0
- package/src/components/waka-artifact-list/index.tsx +416 -0
- package/src/components/waka-badge-showcase/index.tsx +12 -11
- package/src/components/waka-build-matrix/index.tsx +396 -0
- package/src/components/waka-command-bar/index.tsx +2 -1
- package/src/components/waka-config-comparator/index.tsx +416 -0
- package/src/components/waka-container-list/index.tsx +475 -0
- package/src/components/waka-content-recommendation/index.tsx +294 -0
- package/src/components/waka-cost-breakdown/index.tsx +10 -10
- package/src/components/waka-database-card/index.tsx +473 -0
- package/src/components/waka-dependency-tree/index.tsx +542 -0
- package/src/components/waka-env-var-editor/index.tsx +417 -0
- package/src/components/waka-feature-flag-row/index.tsx +386 -0
- package/src/components/waka-funnel-chart/index.tsx +8 -8
- package/src/components/waka-health-pulse/index.tsx +6 -6
- package/src/components/waka-kubernetes-overview/index.tsx +536 -0
- package/src/components/waka-leaderboard/index.tsx +9 -9
- package/src/components/waka-log-viewer/index.tsx +386 -0
- package/src/components/waka-loot-box/index.tsx +20 -20
- package/src/components/waka-migration-list/index.tsx +487 -0
- package/src/components/waka-outstream-video/index.tsx +240 -0
- package/src/components/waka-player-card/index.tsx +5 -5
- package/src/components/waka-pod-card/index.tsx +528 -0
- package/src/components/waka-query-explain/index.tsx +657 -0
- package/src/components/waka-quota-bar/index.tsx +4 -4
- package/src/components/waka-radar-score/index.tsx +10 -10
- package/src/components/waka-scratch-card/index.tsx +5 -4
- package/src/components/waka-secret-card/index.tsx +371 -0
- package/src/components/waka-security-scan-result/index.tsx +473 -0
- package/src/components/waka-server-rack/index.tsx +28 -27
- package/src/components/waka-service-graph/index.tsx +445 -0
- package/src/components/waka-sponsored-badge/index.tsx +97 -0
- package/src/components/waka-sponsored-card/index.tsx +275 -0
- package/src/components/waka-sponsored-feed/index.tsx +127 -0
- package/src/components/waka-spotlight/index.tsx +2 -1
- package/src/components/waka-success-explosion/index.tsx +4 -4
- package/src/components/waka-test-report/index.tsx +469 -0
- package/src/components/waka-trace-viewer/index.tsx +490 -0
- package/src/components/waka-video-ad/index.tsx +406 -0
- package/src/components/waka-video-overlay/index.tsx +257 -0
- package/src/components/waka-xp-bar/index.tsx +13 -13
- package/src/styles/base.css +16 -0
- package/src/styles/tailwind.preset.js +12 -0
- package/src/styles/themes/forest.css +16 -0
- package/src/styles/themes/monochrome.css +16 -0
- package/src/styles/themes/perpetuity.css +16 -0
- package/src/styles/themes/sunset.css +16 -0
- package/src/styles/themes/twilight.css +16 -0
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "../../utils/cn"
|
|
5
|
+
import { Badge } from "../../components/badge"
|
|
6
|
+
import { Button } from "../../components/button"
|
|
7
|
+
import { Card, CardContent, CardHeader, CardTitle } from "../../components/card"
|
|
8
|
+
import { Progress } from "../../components/progress"
|
|
9
|
+
import { Input } from "../../components/input"
|
|
10
|
+
import { Textarea } from "../../components/textarea"
|
|
11
|
+
import { ScrollArea } from "../../components/scroll-area"
|
|
12
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../components/tabs"
|
|
13
|
+
import {
|
|
14
|
+
Select,
|
|
15
|
+
SelectContent,
|
|
16
|
+
SelectItem,
|
|
17
|
+
SelectTrigger,
|
|
18
|
+
SelectValue,
|
|
19
|
+
} from "../../components/select"
|
|
20
|
+
import {
|
|
21
|
+
Database,
|
|
22
|
+
Play,
|
|
23
|
+
RefreshCw,
|
|
24
|
+
Download,
|
|
25
|
+
Upload,
|
|
26
|
+
Clock,
|
|
27
|
+
AlertTriangle,
|
|
28
|
+
CheckCircle2,
|
|
29
|
+
XCircle,
|
|
30
|
+
Activity,
|
|
31
|
+
Users,
|
|
32
|
+
HardDrive,
|
|
33
|
+
Zap,
|
|
34
|
+
ArrowUp,
|
|
35
|
+
ArrowDown,
|
|
36
|
+
Search,
|
|
37
|
+
Copy,
|
|
38
|
+
Trash2,
|
|
39
|
+
History,
|
|
40
|
+
Settings,
|
|
41
|
+
FileText,
|
|
42
|
+
Table2,
|
|
43
|
+
RotateCcw,
|
|
44
|
+
Terminal,
|
|
45
|
+
} from "lucide-react"
|
|
46
|
+
|
|
47
|
+
export type DatabaseStatus = "healthy" | "warning" | "critical" | "offline"
|
|
48
|
+
export type BackupStatus = "completed" | "running" | "failed" | "scheduled"
|
|
49
|
+
export type QueryStatus = "success" | "error" | "running"
|
|
50
|
+
|
|
51
|
+
export interface DatabaseConnection {
|
|
52
|
+
id: string
|
|
53
|
+
user: string
|
|
54
|
+
database: string
|
|
55
|
+
state: "active" | "idle" | "idle_in_transaction" | "waiting"
|
|
56
|
+
query?: string
|
|
57
|
+
duration: number // in seconds
|
|
58
|
+
client: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface DatabaseBackup {
|
|
62
|
+
id: string
|
|
63
|
+
name: string
|
|
64
|
+
type: "full" | "incremental" | "point_in_time"
|
|
65
|
+
status: BackupStatus
|
|
66
|
+
size?: number // in bytes
|
|
67
|
+
startedAt: Date
|
|
68
|
+
completedAt?: Date
|
|
69
|
+
duration?: number // in seconds
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface SlowQuery {
|
|
73
|
+
id: string
|
|
74
|
+
query: string
|
|
75
|
+
calls: number
|
|
76
|
+
totalTime: number // in ms
|
|
77
|
+
meanTime: number // in ms
|
|
78
|
+
maxTime: number // in ms
|
|
79
|
+
rows: number
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface DatabaseMetrics {
|
|
83
|
+
connections: {
|
|
84
|
+
active: number
|
|
85
|
+
idle: number
|
|
86
|
+
max: number
|
|
87
|
+
}
|
|
88
|
+
transactions: {
|
|
89
|
+
committed: number
|
|
90
|
+
rolledBack: number
|
|
91
|
+
perSecond: number
|
|
92
|
+
}
|
|
93
|
+
cache: {
|
|
94
|
+
hitRatio: number
|
|
95
|
+
bufferHit: number
|
|
96
|
+
diskRead: number
|
|
97
|
+
}
|
|
98
|
+
storage: {
|
|
99
|
+
used: number // bytes
|
|
100
|
+
total: number // bytes
|
|
101
|
+
tablespace: number // bytes
|
|
102
|
+
indexes: number // bytes
|
|
103
|
+
}
|
|
104
|
+
replication?: {
|
|
105
|
+
lag: number // in ms
|
|
106
|
+
state: "streaming" | "catchup" | "disconnected"
|
|
107
|
+
replicas: number
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface DatabaseInstance {
|
|
112
|
+
id: string
|
|
113
|
+
name: string
|
|
114
|
+
type: "postgresql" | "mysql" | "mongodb" | "redis"
|
|
115
|
+
version: string
|
|
116
|
+
host: string
|
|
117
|
+
port: number
|
|
118
|
+
status: DatabaseStatus
|
|
119
|
+
role: "primary" | "replica" | "standalone"
|
|
120
|
+
metrics: DatabaseMetrics
|
|
121
|
+
connections: DatabaseConnection[]
|
|
122
|
+
backups: DatabaseBackup[]
|
|
123
|
+
slowQueries: SlowQuery[]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface DatabaseAdminProps {
|
|
127
|
+
instance: DatabaseInstance
|
|
128
|
+
onExecuteQuery?: (query: string) => void
|
|
129
|
+
onCreateBackup?: () => void
|
|
130
|
+
onRestoreBackup?: (backup: DatabaseBackup) => void
|
|
131
|
+
onKillConnection?: (connection: DatabaseConnection) => void
|
|
132
|
+
onRefresh?: () => void
|
|
133
|
+
className?: string
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const statusConfig: Record<DatabaseStatus, { color: string; bgColor: string; label: string }> = {
|
|
137
|
+
healthy: { color: "text-green-500", bgColor: "bg-green-500", label: "Healthy" },
|
|
138
|
+
warning: { color: "text-yellow-500", bgColor: "bg-yellow-500", label: "Warning" },
|
|
139
|
+
critical: { color: "text-red-500", bgColor: "bg-red-500", label: "Critical" },
|
|
140
|
+
offline: { color: "text-gray-500", bgColor: "bg-gray-500", label: "Offline" },
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const connectionStateConfig = {
|
|
144
|
+
active: { color: "text-green-500", label: "Active" },
|
|
145
|
+
idle: { color: "text-gray-500", label: "Idle" },
|
|
146
|
+
idle_in_transaction: { color: "text-yellow-500", label: "Idle in Transaction" },
|
|
147
|
+
waiting: { color: "text-orange-500", label: "Waiting" },
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function formatBytes(bytes: number): string {
|
|
151
|
+
if (bytes < 1024) return `${bytes} B`
|
|
152
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
153
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
154
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function formatDuration(seconds: number): string {
|
|
158
|
+
if (seconds < 60) return `${seconds.toFixed(0)}s`
|
|
159
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${Math.floor(seconds % 60)}s`
|
|
160
|
+
return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function formatMs(ms: number): string {
|
|
164
|
+
if (ms < 1) return `${(ms * 1000).toFixed(0)}µs`
|
|
165
|
+
if (ms < 1000) return `${ms.toFixed(2)}ms`
|
|
166
|
+
return `${(ms / 1000).toFixed(2)}s`
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function MetricsOverview({ metrics, status }: { metrics: DatabaseMetrics; status: DatabaseStatus }) {
|
|
170
|
+
const statusConf = statusConfig[status]
|
|
171
|
+
const connectionPercent = (metrics.connections.active / metrics.connections.max) * 100
|
|
172
|
+
const storagePercent = (metrics.storage.used / metrics.storage.total) * 100
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<div className="grid grid-cols-4 gap-4">
|
|
176
|
+
{/* Connections */}
|
|
177
|
+
<Card>
|
|
178
|
+
<CardContent className="p-4">
|
|
179
|
+
<div className="flex items-center justify-between mb-2">
|
|
180
|
+
<Users className="h-4 w-4 text-muted-foreground" />
|
|
181
|
+
<div className={cn("w-2 h-2 rounded-full", statusConf.bgColor)} />
|
|
182
|
+
</div>
|
|
183
|
+
<div className="text-2xl font-bold">{metrics.connections.active}</div>
|
|
184
|
+
<div className="text-sm text-muted-foreground">Active Connections</div>
|
|
185
|
+
<div className="mt-2">
|
|
186
|
+
<div className="flex justify-between text-xs mb-1">
|
|
187
|
+
<span>{metrics.connections.idle} idle</span>
|
|
188
|
+
<span>max {metrics.connections.max}</span>
|
|
189
|
+
</div>
|
|
190
|
+
<Progress
|
|
191
|
+
value={connectionPercent}
|
|
192
|
+
className={cn("h-1.5", connectionPercent > 80 && "[&>div]:bg-red-500")}
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
</CardContent>
|
|
196
|
+
</Card>
|
|
197
|
+
|
|
198
|
+
{/* Transactions */}
|
|
199
|
+
<Card>
|
|
200
|
+
<CardContent className="p-4">
|
|
201
|
+
<div className="flex items-center justify-between mb-2">
|
|
202
|
+
<Zap className="h-4 w-4 text-muted-foreground" />
|
|
203
|
+
<Badge variant="secondary" className="text-xs">
|
|
204
|
+
{metrics.transactions.perSecond}/s
|
|
205
|
+
</Badge>
|
|
206
|
+
</div>
|
|
207
|
+
<div className="text-2xl font-bold">{metrics.transactions.committed}</div>
|
|
208
|
+
<div className="text-sm text-muted-foreground">Transactions</div>
|
|
209
|
+
<div className="mt-2 text-xs text-muted-foreground">
|
|
210
|
+
<span className="text-red-500">{metrics.transactions.rolledBack}</span> rolled back
|
|
211
|
+
</div>
|
|
212
|
+
</CardContent>
|
|
213
|
+
</Card>
|
|
214
|
+
|
|
215
|
+
{/* Cache */}
|
|
216
|
+
<Card>
|
|
217
|
+
<CardContent className="p-4">
|
|
218
|
+
<div className="flex items-center justify-between mb-2">
|
|
219
|
+
<Activity className="h-4 w-4 text-muted-foreground" />
|
|
220
|
+
</div>
|
|
221
|
+
<div className={cn(
|
|
222
|
+
"text-2xl font-bold",
|
|
223
|
+
metrics.cache.hitRatio < 90 && "text-yellow-500",
|
|
224
|
+
metrics.cache.hitRatio < 80 && "text-red-500"
|
|
225
|
+
)}>
|
|
226
|
+
{metrics.cache.hitRatio.toFixed(1)}%
|
|
227
|
+
</div>
|
|
228
|
+
<div className="text-sm text-muted-foreground">Cache Hit Ratio</div>
|
|
229
|
+
<div className="mt-2 text-xs text-muted-foreground">
|
|
230
|
+
Buffer: {metrics.cache.bufferHit} • Disk: {metrics.cache.diskRead}
|
|
231
|
+
</div>
|
|
232
|
+
</CardContent>
|
|
233
|
+
</Card>
|
|
234
|
+
|
|
235
|
+
{/* Storage */}
|
|
236
|
+
<Card>
|
|
237
|
+
<CardContent className="p-4">
|
|
238
|
+
<div className="flex items-center justify-between mb-2">
|
|
239
|
+
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
|
240
|
+
</div>
|
|
241
|
+
<div className="text-2xl font-bold">{formatBytes(metrics.storage.used)}</div>
|
|
242
|
+
<div className="text-sm text-muted-foreground">Storage Used</div>
|
|
243
|
+
<div className="mt-2">
|
|
244
|
+
<Progress
|
|
245
|
+
value={storagePercent}
|
|
246
|
+
className={cn("h-1.5", storagePercent > 80 && "[&>div]:bg-red-500")}
|
|
247
|
+
/>
|
|
248
|
+
<div className="text-xs text-muted-foreground mt-1">
|
|
249
|
+
of {formatBytes(metrics.storage.total)}
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
</CardContent>
|
|
253
|
+
</Card>
|
|
254
|
+
</div>
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function ConnectionsTable({
|
|
259
|
+
connections,
|
|
260
|
+
onKill,
|
|
261
|
+
}: {
|
|
262
|
+
connections: DatabaseConnection[]
|
|
263
|
+
onKill?: (conn: DatabaseConnection) => void
|
|
264
|
+
}) {
|
|
265
|
+
return (
|
|
266
|
+
<Card>
|
|
267
|
+
<CardHeader className="pb-2">
|
|
268
|
+
<div className="flex items-center justify-between">
|
|
269
|
+
<CardTitle className="text-base flex items-center gap-2">
|
|
270
|
+
<Users className="h-4 w-4" />
|
|
271
|
+
Active Connections
|
|
272
|
+
</CardTitle>
|
|
273
|
+
<Badge variant="secondary">{connections.length}</Badge>
|
|
274
|
+
</div>
|
|
275
|
+
</CardHeader>
|
|
276
|
+
<CardContent>
|
|
277
|
+
<ScrollArea className="h-[300px]">
|
|
278
|
+
<div className="space-y-2">
|
|
279
|
+
{connections.map((conn) => {
|
|
280
|
+
const stateConf = connectionStateConfig[conn.state]
|
|
281
|
+
|
|
282
|
+
return (
|
|
283
|
+
<div
|
|
284
|
+
key={conn.id}
|
|
285
|
+
className={cn(
|
|
286
|
+
"flex items-center gap-3 p-2 rounded-lg border",
|
|
287
|
+
conn.state === "idle_in_transaction" && "border-yellow-500/30",
|
|
288
|
+
conn.duration > 60 && "bg-yellow-500/5"
|
|
289
|
+
)}
|
|
290
|
+
>
|
|
291
|
+
<div className="flex-1 min-w-0">
|
|
292
|
+
<div className="flex items-center gap-2">
|
|
293
|
+
<span className="font-medium">{conn.user}</span>
|
|
294
|
+
<Badge variant="outline" className={cn("text-xs", stateConf.color)}>
|
|
295
|
+
{stateConf.label}
|
|
296
|
+
</Badge>
|
|
297
|
+
<span className="text-xs text-muted-foreground">
|
|
298
|
+
@ {conn.database}
|
|
299
|
+
</span>
|
|
300
|
+
</div>
|
|
301
|
+
{conn.query && (
|
|
302
|
+
<code className="text-xs text-muted-foreground block truncate mt-1">
|
|
303
|
+
{conn.query}
|
|
304
|
+
</code>
|
|
305
|
+
)}
|
|
306
|
+
</div>
|
|
307
|
+
|
|
308
|
+
<div className="text-right shrink-0">
|
|
309
|
+
<div className={cn(
|
|
310
|
+
"text-sm font-medium",
|
|
311
|
+
conn.duration > 60 && "text-yellow-500",
|
|
312
|
+
conn.duration > 300 && "text-red-500"
|
|
313
|
+
)}>
|
|
314
|
+
{formatDuration(conn.duration)}
|
|
315
|
+
</div>
|
|
316
|
+
<div className="text-xs text-muted-foreground">{conn.client}</div>
|
|
317
|
+
</div>
|
|
318
|
+
|
|
319
|
+
{onKill && conn.state !== "idle" && (
|
|
320
|
+
<Button
|
|
321
|
+
variant="ghost"
|
|
322
|
+
size="sm"
|
|
323
|
+
className="h-8 w-8 p-0 text-red-500"
|
|
324
|
+
onClick={() => onKill(conn)}
|
|
325
|
+
>
|
|
326
|
+
<XCircle className="h-4 w-4" />
|
|
327
|
+
</Button>
|
|
328
|
+
)}
|
|
329
|
+
</div>
|
|
330
|
+
)
|
|
331
|
+
})}
|
|
332
|
+
</div>
|
|
333
|
+
</ScrollArea>
|
|
334
|
+
</CardContent>
|
|
335
|
+
</Card>
|
|
336
|
+
)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function BackupsTable({
|
|
340
|
+
backups,
|
|
341
|
+
onRestore,
|
|
342
|
+
onCreate,
|
|
343
|
+
}: {
|
|
344
|
+
backups: DatabaseBackup[]
|
|
345
|
+
onRestore?: (backup: DatabaseBackup) => void
|
|
346
|
+
onCreate?: () => void
|
|
347
|
+
}) {
|
|
348
|
+
return (
|
|
349
|
+
<Card>
|
|
350
|
+
<CardHeader className="pb-2">
|
|
351
|
+
<div className="flex items-center justify-between">
|
|
352
|
+
<CardTitle className="text-base flex items-center gap-2">
|
|
353
|
+
<Download className="h-4 w-4" />
|
|
354
|
+
Backups
|
|
355
|
+
</CardTitle>
|
|
356
|
+
{onCreate && (
|
|
357
|
+
<Button size="sm" onClick={onCreate}>
|
|
358
|
+
<Upload className="h-4 w-4 mr-1" />
|
|
359
|
+
Create Backup
|
|
360
|
+
</Button>
|
|
361
|
+
)}
|
|
362
|
+
</div>
|
|
363
|
+
</CardHeader>
|
|
364
|
+
<CardContent>
|
|
365
|
+
<ScrollArea className="h-[200px]">
|
|
366
|
+
<div className="space-y-2">
|
|
367
|
+
{backups.map((backup) => (
|
|
368
|
+
<div
|
|
369
|
+
key={backup.id}
|
|
370
|
+
className="flex items-center gap-3 p-2 rounded-lg border"
|
|
371
|
+
>
|
|
372
|
+
{backup.status === "completed" ? (
|
|
373
|
+
<CheckCircle2 className="h-4 w-4 text-green-500 shrink-0" />
|
|
374
|
+
) : backup.status === "failed" ? (
|
|
375
|
+
<XCircle className="h-4 w-4 text-red-500 shrink-0" />
|
|
376
|
+
) : backup.status === "running" ? (
|
|
377
|
+
<RefreshCw className="h-4 w-4 text-blue-500 animate-spin shrink-0" />
|
|
378
|
+
) : (
|
|
379
|
+
<Clock className="h-4 w-4 text-muted-foreground shrink-0" />
|
|
380
|
+
)}
|
|
381
|
+
|
|
382
|
+
<div className="flex-1 min-w-0">
|
|
383
|
+
<div className="flex items-center gap-2">
|
|
384
|
+
<span className="font-medium">{backup.name}</span>
|
|
385
|
+
<Badge variant="outline" className="text-xs">
|
|
386
|
+
{backup.type}
|
|
387
|
+
</Badge>
|
|
388
|
+
</div>
|
|
389
|
+
<div className="text-xs text-muted-foreground">
|
|
390
|
+
{backup.startedAt.toLocaleString()}
|
|
391
|
+
{backup.duration && ` • ${formatDuration(backup.duration)}`}
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
|
|
395
|
+
<div className="text-right shrink-0">
|
|
396
|
+
{backup.size && (
|
|
397
|
+
<div className="text-sm font-medium">{formatBytes(backup.size)}</div>
|
|
398
|
+
)}
|
|
399
|
+
</div>
|
|
400
|
+
|
|
401
|
+
{onRestore && backup.status === "completed" && (
|
|
402
|
+
<Button variant="ghost" size="sm" onClick={() => onRestore(backup)}>
|
|
403
|
+
<RotateCcw className="h-4 w-4" />
|
|
404
|
+
</Button>
|
|
405
|
+
)}
|
|
406
|
+
</div>
|
|
407
|
+
))}
|
|
408
|
+
</div>
|
|
409
|
+
</ScrollArea>
|
|
410
|
+
</CardContent>
|
|
411
|
+
</Card>
|
|
412
|
+
)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function SlowQueriesTable({ queries }: { queries: SlowQuery[] }) {
|
|
416
|
+
const [copiedId, setCopiedId] = React.useState<string | null>(null)
|
|
417
|
+
|
|
418
|
+
const copyQuery = (query: SlowQuery) => {
|
|
419
|
+
navigator.clipboard.writeText(query.query)
|
|
420
|
+
setCopiedId(query.id)
|
|
421
|
+
setTimeout(() => setCopiedId(null), 2000)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return (
|
|
425
|
+
<Card>
|
|
426
|
+
<CardHeader className="pb-2">
|
|
427
|
+
<CardTitle className="text-base flex items-center gap-2">
|
|
428
|
+
<AlertTriangle className="h-4 w-4 text-yellow-500" />
|
|
429
|
+
Slow Queries
|
|
430
|
+
</CardTitle>
|
|
431
|
+
</CardHeader>
|
|
432
|
+
<CardContent>
|
|
433
|
+
<ScrollArea className="h-[300px]">
|
|
434
|
+
<div className="space-y-2">
|
|
435
|
+
{queries.map((query) => (
|
|
436
|
+
<div
|
|
437
|
+
key={query.id}
|
|
438
|
+
className="p-2 rounded-lg border bg-muted/30"
|
|
439
|
+
>
|
|
440
|
+
<div className="flex items-start justify-between gap-2">
|
|
441
|
+
<code className="text-xs font-mono line-clamp-2 flex-1">
|
|
442
|
+
{query.query}
|
|
443
|
+
</code>
|
|
444
|
+
<Button
|
|
445
|
+
variant="ghost"
|
|
446
|
+
size="sm"
|
|
447
|
+
className="h-6 w-6 p-0 shrink-0"
|
|
448
|
+
onClick={() => copyQuery(query)}
|
|
449
|
+
>
|
|
450
|
+
{copiedId === query.id ? (
|
|
451
|
+
<CheckCircle2 className="h-3 w-3 text-green-500" />
|
|
452
|
+
) : (
|
|
453
|
+
<Copy className="h-3 w-3" />
|
|
454
|
+
)}
|
|
455
|
+
</Button>
|
|
456
|
+
</div>
|
|
457
|
+
<div className="flex items-center gap-4 mt-2 text-xs text-muted-foreground">
|
|
458
|
+
<span>Calls: <strong>{query.calls}</strong></span>
|
|
459
|
+
<span>Mean: <strong className="text-yellow-500">{formatMs(query.meanTime)}</strong></span>
|
|
460
|
+
<span>Max: <strong className="text-red-500">{formatMs(query.maxTime)}</strong></span>
|
|
461
|
+
<span>Total: <strong>{formatMs(query.totalTime)}</strong></span>
|
|
462
|
+
<span>Rows: <strong>{query.rows}</strong></span>
|
|
463
|
+
</div>
|
|
464
|
+
</div>
|
|
465
|
+
))}
|
|
466
|
+
</div>
|
|
467
|
+
</ScrollArea>
|
|
468
|
+
</CardContent>
|
|
469
|
+
</Card>
|
|
470
|
+
)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function QueryEditor({
|
|
474
|
+
onExecute,
|
|
475
|
+
}: {
|
|
476
|
+
onExecute?: (query: string) => void
|
|
477
|
+
}) {
|
|
478
|
+
const [query, setQuery] = React.useState("")
|
|
479
|
+
const [history, setHistory] = React.useState<string[]>([])
|
|
480
|
+
|
|
481
|
+
const handleExecute = () => {
|
|
482
|
+
if (query.trim()) {
|
|
483
|
+
onExecute?.(query)
|
|
484
|
+
setHistory((prev) => [query, ...prev.slice(0, 9)])
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return (
|
|
489
|
+
<Card>
|
|
490
|
+
<CardHeader className="pb-2">
|
|
491
|
+
<CardTitle className="text-base flex items-center gap-2">
|
|
492
|
+
<Terminal className="h-4 w-4" />
|
|
493
|
+
Query Editor
|
|
494
|
+
</CardTitle>
|
|
495
|
+
</CardHeader>
|
|
496
|
+
<CardContent className="space-y-3">
|
|
497
|
+
<Textarea
|
|
498
|
+
value={query}
|
|
499
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
500
|
+
placeholder="SELECT * FROM users LIMIT 10;"
|
|
501
|
+
className="font-mono text-sm min-h-[120px]"
|
|
502
|
+
/>
|
|
503
|
+
<div className="flex items-center justify-between">
|
|
504
|
+
<div className="flex items-center gap-2">
|
|
505
|
+
{history.length > 0 && (
|
|
506
|
+
<Select onValueChange={(v) => setQuery(v)}>
|
|
507
|
+
<SelectTrigger className="w-40">
|
|
508
|
+
<History className="h-4 w-4 mr-2" />
|
|
509
|
+
History
|
|
510
|
+
</SelectTrigger>
|
|
511
|
+
<SelectContent>
|
|
512
|
+
{history.map((q, i) => (
|
|
513
|
+
<SelectItem key={i} value={q}>
|
|
514
|
+
<span className="truncate max-w-[300px]">{q}</span>
|
|
515
|
+
</SelectItem>
|
|
516
|
+
))}
|
|
517
|
+
</SelectContent>
|
|
518
|
+
</Select>
|
|
519
|
+
)}
|
|
520
|
+
</div>
|
|
521
|
+
<Button onClick={handleExecute} disabled={!query.trim()}>
|
|
522
|
+
<Play className="h-4 w-4 mr-1" />
|
|
523
|
+
Execute
|
|
524
|
+
</Button>
|
|
525
|
+
</div>
|
|
526
|
+
</CardContent>
|
|
527
|
+
</Card>
|
|
528
|
+
)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export function DatabaseAdmin({
|
|
532
|
+
instance,
|
|
533
|
+
onExecuteQuery,
|
|
534
|
+
onCreateBackup,
|
|
535
|
+
onRestoreBackup,
|
|
536
|
+
onKillConnection,
|
|
537
|
+
onRefresh,
|
|
538
|
+
className,
|
|
539
|
+
}: DatabaseAdminProps) {
|
|
540
|
+
const statusConf = statusConfig[instance.status]
|
|
541
|
+
|
|
542
|
+
return (
|
|
543
|
+
<div className={cn("space-y-6", className)}>
|
|
544
|
+
{/* Header */}
|
|
545
|
+
<Card>
|
|
546
|
+
<CardHeader>
|
|
547
|
+
<div className="flex items-center justify-between">
|
|
548
|
+
<div className="flex items-center gap-3">
|
|
549
|
+
<Database className="h-6 w-6" />
|
|
550
|
+
<div>
|
|
551
|
+
<div className="flex items-center gap-2">
|
|
552
|
+
<CardTitle>{instance.name}</CardTitle>
|
|
553
|
+
<Badge className={cn("text-white", statusConf.bgColor)}>
|
|
554
|
+
{statusConf.label}
|
|
555
|
+
</Badge>
|
|
556
|
+
<Badge variant="outline">{instance.role}</Badge>
|
|
557
|
+
</div>
|
|
558
|
+
<p className="text-sm text-muted-foreground">
|
|
559
|
+
{instance.type} {instance.version} • {instance.host}:{instance.port}
|
|
560
|
+
</p>
|
|
561
|
+
</div>
|
|
562
|
+
</div>
|
|
563
|
+
|
|
564
|
+
<div className="flex items-center gap-2">
|
|
565
|
+
{instance.metrics.replication && (
|
|
566
|
+
<Badge
|
|
567
|
+
variant="outline"
|
|
568
|
+
className={cn(
|
|
569
|
+
instance.metrics.replication.lag > 1000 ? "text-red-500" :
|
|
570
|
+
instance.metrics.replication.lag > 100 ? "text-yellow-500" :
|
|
571
|
+
"text-green-500"
|
|
572
|
+
)}
|
|
573
|
+
>
|
|
574
|
+
Replication: {formatMs(instance.metrics.replication.lag)} lag
|
|
575
|
+
</Badge>
|
|
576
|
+
)}
|
|
577
|
+
{onRefresh && (
|
|
578
|
+
<Button variant="outline" onClick={onRefresh}>
|
|
579
|
+
<RefreshCw className="h-4 w-4" />
|
|
580
|
+
</Button>
|
|
581
|
+
)}
|
|
582
|
+
</div>
|
|
583
|
+
</div>
|
|
584
|
+
</CardHeader>
|
|
585
|
+
</Card>
|
|
586
|
+
|
|
587
|
+
{/* Metrics */}
|
|
588
|
+
<MetricsOverview metrics={instance.metrics} status={instance.status} />
|
|
589
|
+
|
|
590
|
+
{/* Tabs */}
|
|
591
|
+
<Tabs defaultValue="connections">
|
|
592
|
+
<TabsList>
|
|
593
|
+
<TabsTrigger value="connections">
|
|
594
|
+
<Users className="h-4 w-4 mr-1" />
|
|
595
|
+
Connections
|
|
596
|
+
</TabsTrigger>
|
|
597
|
+
<TabsTrigger value="queries">
|
|
598
|
+
<Terminal className="h-4 w-4 mr-1" />
|
|
599
|
+
Query
|
|
600
|
+
</TabsTrigger>
|
|
601
|
+
<TabsTrigger value="slow">
|
|
602
|
+
<AlertTriangle className="h-4 w-4 mr-1" />
|
|
603
|
+
Slow Queries
|
|
604
|
+
</TabsTrigger>
|
|
605
|
+
<TabsTrigger value="backups">
|
|
606
|
+
<Download className="h-4 w-4 mr-1" />
|
|
607
|
+
Backups
|
|
608
|
+
</TabsTrigger>
|
|
609
|
+
</TabsList>
|
|
610
|
+
|
|
611
|
+
<TabsContent value="connections" className="mt-4">
|
|
612
|
+
<ConnectionsTable
|
|
613
|
+
connections={instance.connections}
|
|
614
|
+
onKill={onKillConnection}
|
|
615
|
+
/>
|
|
616
|
+
</TabsContent>
|
|
617
|
+
|
|
618
|
+
<TabsContent value="queries" className="mt-4">
|
|
619
|
+
<QueryEditor onExecute={onExecuteQuery} />
|
|
620
|
+
</TabsContent>
|
|
621
|
+
|
|
622
|
+
<TabsContent value="slow" className="mt-4">
|
|
623
|
+
<SlowQueriesTable queries={instance.slowQueries} />
|
|
624
|
+
</TabsContent>
|
|
625
|
+
|
|
626
|
+
<TabsContent value="backups" className="mt-4">
|
|
627
|
+
<BackupsTable
|
|
628
|
+
backups={instance.backups}
|
|
629
|
+
onRestore={onRestoreBackup}
|
|
630
|
+
onCreate={onCreateBackup}
|
|
631
|
+
/>
|
|
632
|
+
</TabsContent>
|
|
633
|
+
</Tabs>
|
|
634
|
+
</div>
|
|
635
|
+
)
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Default sample data
|
|
639
|
+
export const defaultDatabaseInstance: DatabaseInstance = {
|
|
640
|
+
id: "1",
|
|
641
|
+
name: "production-db",
|
|
642
|
+
type: "postgresql",
|
|
643
|
+
version: "15.4",
|
|
644
|
+
host: "db.example.com",
|
|
645
|
+
port: 5432,
|
|
646
|
+
status: "healthy",
|
|
647
|
+
role: "primary",
|
|
648
|
+
metrics: {
|
|
649
|
+
connections: { active: 45, idle: 25, max: 100 },
|
|
650
|
+
transactions: { committed: 125000, rolledBack: 15, perSecond: 250 },
|
|
651
|
+
cache: { hitRatio: 98.5, bufferHit: 15000, diskRead: 250 },
|
|
652
|
+
storage: {
|
|
653
|
+
used: 85 * 1024 * 1024 * 1024,
|
|
654
|
+
total: 200 * 1024 * 1024 * 1024,
|
|
655
|
+
tablespace: 75 * 1024 * 1024 * 1024,
|
|
656
|
+
indexes: 10 * 1024 * 1024 * 1024,
|
|
657
|
+
},
|
|
658
|
+
replication: { lag: 25, state: "streaming", replicas: 2 },
|
|
659
|
+
},
|
|
660
|
+
connections: [
|
|
661
|
+
{ id: "1", user: "api_user", database: "production", state: "active", query: "SELECT * FROM users WHERE id = $1", duration: 0.5, client: "192.168.1.10" },
|
|
662
|
+
{ id: "2", user: "api_user", database: "production", state: "active", query: "UPDATE orders SET status = $1 WHERE id = $2", duration: 1.2, client: "192.168.1.11" },
|
|
663
|
+
{ id: "3", user: "analytics", database: "production", state: "idle_in_transaction", query: "SELECT COUNT(*) FROM events WHERE...", duration: 125, client: "192.168.1.20" },
|
|
664
|
+
{ id: "4", user: "admin", database: "production", state: "idle", duration: 300, client: "192.168.1.5" },
|
|
665
|
+
{ id: "5", user: "api_user", database: "production", state: "active", duration: 0.1, client: "192.168.1.12" },
|
|
666
|
+
],
|
|
667
|
+
backups: [
|
|
668
|
+
{ id: "1", name: "daily-backup-20240110", type: "full", status: "completed", size: 45 * 1024 * 1024 * 1024, startedAt: new Date(Date.now() - 4 * 3600000), completedAt: new Date(Date.now() - 2 * 3600000), duration: 7200 },
|
|
669
|
+
{ id: "2", name: "hourly-backup-20240110-12", type: "incremental", status: "completed", size: 250 * 1024 * 1024, startedAt: new Date(Date.now() - 1 * 3600000), completedAt: new Date(Date.now() - 50 * 60000), duration: 600 },
|
|
670
|
+
{ id: "3", name: "hourly-backup-20240110-13", type: "incremental", status: "running", startedAt: new Date(Date.now() - 5 * 60000) },
|
|
671
|
+
{ id: "4", name: "daily-backup-20240111", type: "full", status: "scheduled", startedAt: new Date(Date.now() + 8 * 3600000) },
|
|
672
|
+
],
|
|
673
|
+
slowQueries: [
|
|
674
|
+
{ id: "1", query: "SELECT * FROM orders o JOIN order_items oi ON o.id = oi.order_id WHERE o.created_at > $1 AND o.status = $2", calls: 1250, totalTime: 45000, meanTime: 36, maxTime: 520, rows: 15000 },
|
|
675
|
+
{ id: "2", query: "SELECT u.*, COUNT(o.id) as order_count FROM users u LEFT JOIN orders o ON u.id = o.user_id GROUP BY u.id ORDER BY order_count DESC LIMIT 100", calls: 500, totalTime: 85000, meanTime: 170, maxTime: 1200, rows: 100 },
|
|
676
|
+
{ id: "3", query: "UPDATE products SET view_count = view_count + 1 WHERE id = $1", calls: 50000, totalTime: 25000, meanTime: 0.5, maxTime: 15, rows: 1 },
|
|
677
|
+
{ id: "4", query: "SELECT * FROM analytics_events WHERE event_type = $1 AND created_at BETWEEN $2 AND $3", calls: 250, totalTime: 125000, meanTime: 500, maxTime: 3500, rows: 50000 },
|
|
678
|
+
],
|
|
679
|
+
}
|