@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.
Files changed (123) hide show
  1. package/dist/blocks/apm-overview/index.d.ts +58 -0
  2. package/dist/blocks/cicd-builder/index.d.ts +47 -0
  3. package/dist/blocks/cloud-cost-dashboard/index.d.ts +49 -0
  4. package/dist/blocks/container-orchestrator/index.d.ts +63 -0
  5. package/dist/blocks/database-admin/index.d.ts +84 -0
  6. package/dist/blocks/gitops-sync-status/index.d.ts +45 -0
  7. package/dist/blocks/incident-manager/index.d.ts +44 -0
  8. package/dist/blocks/index.d.ts +10 -0
  9. package/dist/blocks/infrastructure-map/index.d.ts +32 -0
  10. package/dist/blocks/on-call-schedule/index.d.ts +43 -0
  11. package/dist/blocks/release-notes/index.d.ts +49 -0
  12. package/dist/components/index.d.ts +34 -0
  13. package/dist/components/waka-ad-banner/index.d.ts +36 -0
  14. package/dist/components/waka-ad-fallback/index.d.ts +33 -0
  15. package/dist/components/waka-ad-inline/index.d.ts +15 -0
  16. package/dist/components/waka-ad-interstitial/index.d.ts +26 -0
  17. package/dist/components/waka-ad-placeholder/index.d.ts +17 -0
  18. package/dist/components/waka-ad-provider/index.d.ts +103 -0
  19. package/dist/components/waka-ad-sidebar/index.d.ts +18 -0
  20. package/dist/components/waka-ad-sticky-footer/index.d.ts +17 -0
  21. package/dist/components/waka-alert-panel/index.d.ts +45 -0
  22. package/dist/components/waka-artifact-list/index.d.ts +32 -0
  23. package/dist/components/waka-build-matrix/index.d.ts +36 -0
  24. package/dist/components/waka-config-comparator/index.d.ts +37 -0
  25. package/dist/components/waka-container-list/index.d.ts +51 -0
  26. package/dist/components/waka-content-recommendation/index.d.ts +23 -0
  27. package/dist/components/waka-database-card/index.d.ts +46 -0
  28. package/dist/components/waka-dependency-tree/index.d.ts +38 -0
  29. package/dist/components/waka-env-var-editor/index.d.ts +30 -0
  30. package/dist/components/waka-feature-flag-row/index.d.ts +45 -0
  31. package/dist/components/waka-kubernetes-overview/index.d.ts +98 -0
  32. package/dist/components/waka-log-viewer/index.d.ts +38 -0
  33. package/dist/components/waka-migration-list/index.d.ts +36 -0
  34. package/dist/components/waka-outstream-video/index.d.ts +24 -0
  35. package/dist/components/waka-pod-card/index.d.ts +73 -0
  36. package/dist/components/waka-query-explain/index.d.ts +48 -0
  37. package/dist/components/waka-secret-card/index.d.ts +43 -0
  38. package/dist/components/waka-security-scan-result/index.d.ts +45 -0
  39. package/dist/components/waka-service-graph/index.d.ts +44 -0
  40. package/dist/components/waka-sponsored-badge/index.d.ts +20 -0
  41. package/dist/components/waka-sponsored-card/index.d.ts +25 -0
  42. package/dist/components/waka-sponsored-feed/index.d.ts +31 -0
  43. package/dist/components/waka-test-report/index.d.ts +60 -0
  44. package/dist/components/waka-trace-viewer/index.d.ts +36 -0
  45. package/dist/components/waka-video-ad/index.d.ts +32 -0
  46. package/dist/components/waka-video-overlay/index.d.ts +26 -0
  47. package/dist/index.cjs.js +251 -200
  48. package/dist/index.d.ts +1 -0
  49. package/dist/index.es.js +47315 -35823
  50. package/dist/utils/security.d.ts +96 -0
  51. package/package.json +4 -4
  52. package/src/blocks/apm-overview/index.tsx +672 -0
  53. package/src/blocks/cicd-builder/index.tsx +738 -0
  54. package/src/blocks/cloud-cost-dashboard/index.tsx +597 -0
  55. package/src/blocks/container-orchestrator/index.tsx +729 -0
  56. package/src/blocks/database-admin/index.tsx +679 -0
  57. package/src/blocks/gitops-sync-status/index.tsx +557 -0
  58. package/src/blocks/incident-manager/index.tsx +586 -0
  59. package/src/blocks/index.ts +119 -0
  60. package/src/blocks/infrastructure-map/index.tsx +638 -0
  61. package/src/blocks/on-call-schedule/index.tsx +615 -0
  62. package/src/blocks/release-notes/index.tsx +643 -0
  63. package/src/blocks/sidebar/index.tsx +6 -6
  64. package/src/components/DataTable/templates/index.tsx +3 -2
  65. package/src/components/index.ts +283 -0
  66. package/src/components/waka-3d-pie-chart/index.tsx +11 -11
  67. package/src/components/waka-achievement-unlock/index.tsx +16 -16
  68. package/src/components/waka-ad-banner/index.tsx +275 -0
  69. package/src/components/waka-ad-fallback/index.tsx +181 -0
  70. package/src/components/waka-ad-inline/index.tsx +103 -0
  71. package/src/components/waka-ad-interstitial/index.tsx +278 -0
  72. package/src/components/waka-ad-placeholder/index.tsx +84 -0
  73. package/src/components/waka-ad-provider/index.tsx +329 -0
  74. package/src/components/waka-ad-sidebar/index.tsx +113 -0
  75. package/src/components/waka-ad-sticky-footer/index.tsx +125 -0
  76. package/src/components/waka-alert-panel/index.tsx +493 -0
  77. package/src/components/waka-artifact-list/index.tsx +416 -0
  78. package/src/components/waka-badge-showcase/index.tsx +12 -11
  79. package/src/components/waka-build-matrix/index.tsx +396 -0
  80. package/src/components/waka-command-bar/index.tsx +2 -1
  81. package/src/components/waka-config-comparator/index.tsx +416 -0
  82. package/src/components/waka-container-list/index.tsx +475 -0
  83. package/src/components/waka-content-recommendation/index.tsx +294 -0
  84. package/src/components/waka-cost-breakdown/index.tsx +10 -10
  85. package/src/components/waka-database-card/index.tsx +473 -0
  86. package/src/components/waka-dependency-tree/index.tsx +542 -0
  87. package/src/components/waka-env-var-editor/index.tsx +417 -0
  88. package/src/components/waka-feature-flag-row/index.tsx +386 -0
  89. package/src/components/waka-funnel-chart/index.tsx +8 -8
  90. package/src/components/waka-health-pulse/index.tsx +6 -6
  91. package/src/components/waka-kubernetes-overview/index.tsx +536 -0
  92. package/src/components/waka-leaderboard/index.tsx +9 -9
  93. package/src/components/waka-log-viewer/index.tsx +386 -0
  94. package/src/components/waka-loot-box/index.tsx +20 -20
  95. package/src/components/waka-migration-list/index.tsx +487 -0
  96. package/src/components/waka-outstream-video/index.tsx +240 -0
  97. package/src/components/waka-player-card/index.tsx +5 -5
  98. package/src/components/waka-pod-card/index.tsx +528 -0
  99. package/src/components/waka-query-explain/index.tsx +657 -0
  100. package/src/components/waka-quota-bar/index.tsx +4 -4
  101. package/src/components/waka-radar-score/index.tsx +10 -10
  102. package/src/components/waka-scratch-card/index.tsx +5 -4
  103. package/src/components/waka-secret-card/index.tsx +371 -0
  104. package/src/components/waka-security-scan-result/index.tsx +473 -0
  105. package/src/components/waka-server-rack/index.tsx +28 -27
  106. package/src/components/waka-service-graph/index.tsx +445 -0
  107. package/src/components/waka-sponsored-badge/index.tsx +97 -0
  108. package/src/components/waka-sponsored-card/index.tsx +275 -0
  109. package/src/components/waka-sponsored-feed/index.tsx +127 -0
  110. package/src/components/waka-spotlight/index.tsx +2 -1
  111. package/src/components/waka-success-explosion/index.tsx +4 -4
  112. package/src/components/waka-test-report/index.tsx +469 -0
  113. package/src/components/waka-trace-viewer/index.tsx +490 -0
  114. package/src/components/waka-video-ad/index.tsx +406 -0
  115. package/src/components/waka-video-overlay/index.tsx +257 -0
  116. package/src/components/waka-xp-bar/index.tsx +13 -13
  117. package/src/styles/base.css +16 -0
  118. package/src/styles/tailwind.preset.js +12 -0
  119. package/src/styles/themes/forest.css +16 -0
  120. package/src/styles/themes/monochrome.css +16 -0
  121. package/src/styles/themes/perpetuity.css +16 -0
  122. package/src/styles/themes/sunset.css +16 -0
  123. 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
+ }