@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,657 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../../utils/cn"
5
+ import { Badge } from "../badge"
6
+ import { Button } from "../button"
7
+ import { ScrollArea } from "../scroll-area"
8
+ import {
9
+ Tooltip,
10
+ TooltipContent,
11
+ TooltipProvider,
12
+ TooltipTrigger,
13
+ } from "../tooltip"
14
+ import {
15
+ Search,
16
+ ChevronDown,
17
+ ChevronRight,
18
+ AlertTriangle,
19
+ Zap,
20
+ Database,
21
+ Table2,
22
+ Filter,
23
+ ArrowDownUp,
24
+ Layers,
25
+ Clock,
26
+ Copy,
27
+ CheckCircle2,
28
+ Info,
29
+ TrendingUp,
30
+ BarChart3,
31
+ } from "lucide-react"
32
+
33
+ export type NodeType =
34
+ | "Seq Scan"
35
+ | "Index Scan"
36
+ | "Index Only Scan"
37
+ | "Bitmap Heap Scan"
38
+ | "Bitmap Index Scan"
39
+ | "Nested Loop"
40
+ | "Hash Join"
41
+ | "Merge Join"
42
+ | "Sort"
43
+ | "Aggregate"
44
+ | "Group"
45
+ | "Limit"
46
+ | "Hash"
47
+ | "Materialize"
48
+ | "Subquery Scan"
49
+ | "CTE Scan"
50
+ | "Result"
51
+ | "Append"
52
+ | "Unique"
53
+ | "WindowAgg"
54
+ | "Gather"
55
+ | "Gather Merge"
56
+
57
+ export interface ExplainNode {
58
+ id: string
59
+ type: NodeType
60
+ relation?: string
61
+ alias?: string
62
+ indexName?: string
63
+ filter?: string
64
+ joinType?: string
65
+ sortKey?: string[]
66
+ startupCost: number
67
+ totalCost: number
68
+ rows: number
69
+ actualRows?: number
70
+ actualTime?: number
71
+ loops?: number
72
+ width?: number
73
+ children?: ExplainNode[]
74
+ warnings?: string[]
75
+ buffers?: {
76
+ sharedHit?: number
77
+ sharedRead?: number
78
+ sharedWritten?: number
79
+ }
80
+ }
81
+
82
+ export interface QueryPlan {
83
+ query: string
84
+ planningTime?: number
85
+ executionTime?: number
86
+ totalCost: number
87
+ rootNode: ExplainNode
88
+ }
89
+
90
+ export interface WakaQueryExplainProps {
91
+ /** Query plan to display */
92
+ plan: QueryPlan
93
+ /** Show actual vs estimated */
94
+ showActual?: boolean
95
+ /** Show buffers info */
96
+ showBuffers?: boolean
97
+ /** Callback when node is clicked */
98
+ onNodeClick?: (node: ExplainNode) => void
99
+ /** Title */
100
+ title?: string
101
+ /** Custom class name */
102
+ className?: string
103
+ }
104
+
105
+ const nodeTypeConfig: Record<NodeType, { icon: React.ElementType; color: string; description: string }> = {
106
+ "Seq Scan": { icon: Table2, color: "text-red-500", description: "Sequential scan (full table scan)" },
107
+ "Index Scan": { icon: Search, color: "text-green-500", description: "Index scan" },
108
+ "Index Only Scan": { icon: Search, color: "text-green-600", description: "Index only scan (no heap access)" },
109
+ "Bitmap Heap Scan": { icon: Table2, color: "text-yellow-500", description: "Bitmap heap scan" },
110
+ "Bitmap Index Scan": { icon: Search, color: "text-yellow-600", description: "Bitmap index scan" },
111
+ "Nested Loop": { icon: Layers, color: "text-blue-500", description: "Nested loop join" },
112
+ "Hash Join": { icon: Layers, color: "text-purple-500", description: "Hash join" },
113
+ "Merge Join": { icon: Layers, color: "text-indigo-500", description: "Merge join" },
114
+ "Sort": { icon: ArrowDownUp, color: "text-orange-500", description: "Sort operation" },
115
+ "Aggregate": { icon: BarChart3, color: "text-cyan-500", description: "Aggregate operation" },
116
+ "Group": { icon: BarChart3, color: "text-cyan-600", description: "Group operation" },
117
+ "Limit": { icon: Filter, color: "text-gray-500", description: "Limit rows" },
118
+ "Hash": { icon: Database, color: "text-purple-400", description: "Hash for join" },
119
+ "Materialize": { icon: Database, color: "text-gray-400", description: "Materialize subquery" },
120
+ "Subquery Scan": { icon: Search, color: "text-gray-500", description: "Subquery scan" },
121
+ "CTE Scan": { icon: Search, color: "text-blue-400", description: "CTE (Common Table Expression) scan" },
122
+ "Result": { icon: Database, color: "text-gray-400", description: "Result node" },
123
+ "Append": { icon: Layers, color: "text-gray-500", description: "Append results" },
124
+ "Unique": { icon: Filter, color: "text-gray-500", description: "Remove duplicates" },
125
+ "WindowAgg": { icon: BarChart3, color: "text-cyan-400", description: "Window function" },
126
+ "Gather": { icon: Layers, color: "text-green-400", description: "Parallel gather" },
127
+ "Gather Merge": { icon: Layers, color: "text-green-500", description: "Parallel gather merge" },
128
+ }
129
+
130
+ function formatCost(cost: number): string {
131
+ if (cost < 1000) return cost.toFixed(2)
132
+ if (cost < 1000000) return `${(cost / 1000).toFixed(1)}k`
133
+ return `${(cost / 1000000).toFixed(1)}M`
134
+ }
135
+
136
+ function formatRows(rows: number): string {
137
+ if (rows < 1000) return rows.toString()
138
+ if (rows < 1000000) return `${(rows / 1000).toFixed(1)}k`
139
+ return `${(rows / 1000000).toFixed(1)}M`
140
+ }
141
+
142
+ function formatTime(ms: number): string {
143
+ if (ms < 1) return `${(ms * 1000).toFixed(0)}µs`
144
+ if (ms < 1000) return `${ms.toFixed(2)}ms`
145
+ return `${(ms / 1000).toFixed(2)}s`
146
+ }
147
+
148
+ function getCostBarWidth(cost: number, maxCost: number): number {
149
+ return Math.max(5, (cost / maxCost) * 100)
150
+ }
151
+
152
+ function ExplainNodeRow({
153
+ node,
154
+ depth,
155
+ maxCost,
156
+ isExpanded,
157
+ onToggle,
158
+ onClick,
159
+ showActual,
160
+ showBuffers,
161
+ }: {
162
+ node: ExplainNode
163
+ depth: number
164
+ maxCost: number
165
+ isExpanded: boolean
166
+ onToggle: () => void
167
+ onClick?: () => void
168
+ showActual?: boolean
169
+ showBuffers?: boolean
170
+ }) {
171
+ const hasChildren = node.children && node.children.length > 0
172
+ const config = nodeTypeConfig[node.type] || { icon: Database, color: "text-gray-500", description: node.type }
173
+ const NodeIcon = config.icon
174
+ const costWidth = getCostBarWidth(node.totalCost, maxCost)
175
+
176
+ const rowEstimateDiff = node.actualRows !== undefined
177
+ ? ((node.actualRows - node.rows) / Math.max(node.rows, 1)) * 100
178
+ : 0
179
+
180
+ return (
181
+ <div
182
+ className={cn(
183
+ "border-b last:border-b-0 hover:bg-muted/30 transition-colors",
184
+ node.warnings && node.warnings.length > 0 && "bg-yellow-500/5"
185
+ )}
186
+ >
187
+ <div
188
+ className="flex items-center gap-2 p-2 cursor-pointer"
189
+ style={{ paddingLeft: `${depth * 20 + 8}px` }}
190
+ onClick={onClick}
191
+ >
192
+ {/* Expand button */}
193
+ <button
194
+ className="w-5 h-5 flex items-center justify-center text-muted-foreground shrink-0"
195
+ onClick={(e) => {
196
+ e.stopPropagation()
197
+ onToggle()
198
+ }}
199
+ disabled={!hasChildren}
200
+ >
201
+ {hasChildren ? (
202
+ isExpanded ? (
203
+ <ChevronDown className="h-4 w-4" />
204
+ ) : (
205
+ <ChevronRight className="h-4 w-4" />
206
+ )
207
+ ) : (
208
+ <span className="w-4" />
209
+ )}
210
+ </button>
211
+
212
+ {/* Node type icon */}
213
+ <TooltipProvider>
214
+ <Tooltip>
215
+ <TooltipTrigger>
216
+ <NodeIcon className={cn("h-4 w-4 shrink-0", config.color)} />
217
+ </TooltipTrigger>
218
+ <TooltipContent>{config.description}</TooltipContent>
219
+ </Tooltip>
220
+ </TooltipProvider>
221
+
222
+ {/* Node info */}
223
+ <div className="flex-1 min-w-0">
224
+ <div className="flex items-center gap-2 flex-wrap">
225
+ <span className="font-medium text-sm">{node.type}</span>
226
+ {node.relation && (
227
+ <Badge variant="outline" className="text-xs">
228
+ {node.alias ? `${node.relation} as ${node.alias}` : node.relation}
229
+ </Badge>
230
+ )}
231
+ {node.indexName && (
232
+ <Badge variant="secondary" className="text-xs text-green-600">
233
+ {node.indexName}
234
+ </Badge>
235
+ )}
236
+ {node.joinType && (
237
+ <Badge variant="outline" className="text-xs">
238
+ {node.joinType}
239
+ </Badge>
240
+ )}
241
+ {node.warnings?.map((warning, i) => (
242
+ <TooltipProvider key={i}>
243
+ <Tooltip>
244
+ <TooltipTrigger>
245
+ <AlertTriangle className="h-4 w-4 text-yellow-500" />
246
+ </TooltipTrigger>
247
+ <TooltipContent>{warning}</TooltipContent>
248
+ </Tooltip>
249
+ </TooltipProvider>
250
+ ))}
251
+ </div>
252
+
253
+ {node.filter && (
254
+ <div className="text-xs text-muted-foreground mt-0.5 font-mono truncate">
255
+ Filter: {node.filter}
256
+ </div>
257
+ )}
258
+
259
+ {node.sortKey && node.sortKey.length > 0 && (
260
+ <div className="text-xs text-muted-foreground mt-0.5 font-mono truncate">
261
+ Sort: {node.sortKey.join(", ")}
262
+ </div>
263
+ )}
264
+ </div>
265
+
266
+ {/* Cost bar */}
267
+ <div className="w-24 shrink-0">
268
+ <div className="h-2 bg-muted rounded overflow-hidden">
269
+ <div
270
+ className={cn(
271
+ "h-full rounded",
272
+ node.type === "Seq Scan" ? "bg-red-500" : "bg-primary"
273
+ )}
274
+ style={{ width: `${costWidth}%` }}
275
+ />
276
+ </div>
277
+ <div className="text-xs text-muted-foreground text-center mt-0.5">
278
+ {formatCost(node.totalCost)}
279
+ </div>
280
+ </div>
281
+
282
+ {/* Rows */}
283
+ <div className="w-20 text-right shrink-0">
284
+ <div className="text-sm font-medium">{formatRows(node.rows)}</div>
285
+ {showActual && node.actualRows !== undefined && (
286
+ <div className={cn(
287
+ "text-xs",
288
+ Math.abs(rowEstimateDiff) > 100 ? "text-red-500" :
289
+ Math.abs(rowEstimateDiff) > 50 ? "text-yellow-500" :
290
+ "text-muted-foreground"
291
+ )}>
292
+ → {formatRows(node.actualRows)}
293
+ </div>
294
+ )}
295
+ </div>
296
+
297
+ {/* Time */}
298
+ {showActual && node.actualTime !== undefined && (
299
+ <div className="w-20 text-right shrink-0">
300
+ <div className="text-sm">{formatTime(node.actualTime)}</div>
301
+ {node.loops && node.loops > 1 && (
302
+ <div className="text-xs text-muted-foreground">
303
+ ×{node.loops}
304
+ </div>
305
+ )}
306
+ </div>
307
+ )}
308
+
309
+ {/* Buffers */}
310
+ {showBuffers && node.buffers && (
311
+ <div className="w-24 text-right shrink-0 text-xs text-muted-foreground">
312
+ {node.buffers.sharedHit !== undefined && (
313
+ <div>Hit: {node.buffers.sharedHit}</div>
314
+ )}
315
+ {node.buffers.sharedRead !== undefined && (
316
+ <div className="text-yellow-500">Read: {node.buffers.sharedRead}</div>
317
+ )}
318
+ </div>
319
+ )}
320
+ </div>
321
+ </div>
322
+ )
323
+ }
324
+
325
+ function ExplainTree({
326
+ node,
327
+ depth,
328
+ maxCost,
329
+ expandedIds,
330
+ onToggle,
331
+ onNodeClick,
332
+ showActual,
333
+ showBuffers,
334
+ }: {
335
+ node: ExplainNode
336
+ depth: number
337
+ maxCost: number
338
+ expandedIds: Set<string>
339
+ onToggle: (id: string) => void
340
+ onNodeClick?: (node: ExplainNode) => void
341
+ showActual?: boolean
342
+ showBuffers?: boolean
343
+ }) {
344
+ const isExpanded = expandedIds.has(node.id)
345
+
346
+ return (
347
+ <>
348
+ <ExplainNodeRow
349
+ node={node}
350
+ depth={depth}
351
+ maxCost={maxCost}
352
+ isExpanded={isExpanded}
353
+ onToggle={() => onToggle(node.id)}
354
+ onClick={() => onNodeClick?.(node)}
355
+ showActual={showActual}
356
+ showBuffers={showBuffers}
357
+ />
358
+ {isExpanded && node.children?.map((child) => (
359
+ <ExplainTree
360
+ key={child.id}
361
+ node={child}
362
+ depth={depth + 1}
363
+ maxCost={maxCost}
364
+ expandedIds={expandedIds}
365
+ onToggle={onToggle}
366
+ onNodeClick={onNodeClick}
367
+ showActual={showActual}
368
+ showBuffers={showBuffers}
369
+ />
370
+ ))}
371
+ </>
372
+ )
373
+ }
374
+
375
+ export function WakaQueryExplain({
376
+ plan,
377
+ showActual = true,
378
+ showBuffers = false,
379
+ onNodeClick,
380
+ title = "Query Plan",
381
+ className,
382
+ }: WakaQueryExplainProps) {
383
+ const [expandedIds, setExpandedIds] = React.useState<Set<string>>(() => {
384
+ // Expand all by default
385
+ const ids = new Set<string>()
386
+ const collectIds = (node: ExplainNode) => {
387
+ ids.add(node.id)
388
+ node.children?.forEach(collectIds)
389
+ }
390
+ collectIds(plan.rootNode)
391
+ return ids
392
+ })
393
+ const [copied, setCopied] = React.useState(false)
394
+
395
+ const toggleExpand = (id: string) => {
396
+ setExpandedIds((prev) => {
397
+ const next = new Set(prev)
398
+ if (next.has(id)) {
399
+ next.delete(id)
400
+ } else {
401
+ next.add(id)
402
+ }
403
+ return next
404
+ })
405
+ }
406
+
407
+ const expandAll = () => {
408
+ const ids = new Set<string>()
409
+ const collectIds = (node: ExplainNode) => {
410
+ ids.add(node.id)
411
+ node.children?.forEach(collectIds)
412
+ }
413
+ collectIds(plan.rootNode)
414
+ setExpandedIds(ids)
415
+ }
416
+
417
+ const collapseAll = () => {
418
+ setExpandedIds(new Set())
419
+ }
420
+
421
+ const copyQuery = () => {
422
+ navigator.clipboard.writeText(plan.query)
423
+ setCopied(true)
424
+ setTimeout(() => setCopied(false), 2000)
425
+ }
426
+
427
+ // Count warnings
428
+ const warningCount = React.useMemo(() => {
429
+ let count = 0
430
+ const countWarnings = (node: ExplainNode) => {
431
+ count += node.warnings?.length || 0
432
+ node.children?.forEach(countWarnings)
433
+ }
434
+ countWarnings(plan.rootNode)
435
+ return count
436
+ }, [plan.rootNode])
437
+
438
+ // Check for seq scans
439
+ const hasSeqScan = React.useMemo(() => {
440
+ const check = (node: ExplainNode): boolean => {
441
+ if (node.type === "Seq Scan") return true
442
+ return node.children?.some(check) || false
443
+ }
444
+ return check(plan.rootNode)
445
+ }, [plan.rootNode])
446
+
447
+ return (
448
+ <div className={cn("flex flex-col border rounded-lg bg-background", className)}>
449
+ {/* Header */}
450
+ <div className="flex items-center justify-between gap-4 p-3 border-b">
451
+ <div className="flex items-center gap-3">
452
+ <Zap className="h-5 w-5" />
453
+ <h3 className="font-semibold">{title}</h3>
454
+ </div>
455
+
456
+ <div className="flex items-center gap-2">
457
+ {plan.planningTime !== undefined && (
458
+ <Badge variant="outline" className="text-xs">
459
+ <Clock className="h-3 w-3 mr-1" />
460
+ Plan: {formatTime(plan.planningTime)}
461
+ </Badge>
462
+ )}
463
+ {plan.executionTime !== undefined && (
464
+ <Badge variant="outline" className="text-xs">
465
+ <Clock className="h-3 w-3 mr-1" />
466
+ Exec: {formatTime(plan.executionTime)}
467
+ </Badge>
468
+ )}
469
+ {hasSeqScan && (
470
+ <Badge className="bg-red-500 text-xs">
471
+ <AlertTriangle className="h-3 w-3 mr-1" />
472
+ Seq Scan
473
+ </Badge>
474
+ )}
475
+ {warningCount > 0 && (
476
+ <Badge className="bg-yellow-500 text-xs">
477
+ {warningCount} warnings
478
+ </Badge>
479
+ )}
480
+ </div>
481
+ </div>
482
+
483
+ {/* Query */}
484
+ <div className="p-3 border-b bg-muted/30">
485
+ <div className="flex items-start justify-between gap-2">
486
+ <pre className="text-xs font-mono whitespace-pre-wrap overflow-x-auto flex-1">
487
+ {plan.query}
488
+ </pre>
489
+ <Button variant="ghost" size="sm" className="h-7 w-7 p-0 shrink-0" onClick={copyQuery}>
490
+ {copied ? (
491
+ <CheckCircle2 className="h-4 w-4 text-green-500" />
492
+ ) : (
493
+ <Copy className="h-4 w-4" />
494
+ )}
495
+ </Button>
496
+ </div>
497
+ </div>
498
+
499
+ {/* Toolbar */}
500
+ <div className="flex items-center gap-2 p-2 border-b">
501
+ <Button variant="ghost" size="sm" className="h-7" onClick={expandAll}>
502
+ Expand All
503
+ </Button>
504
+ <Button variant="ghost" size="sm" className="h-7" onClick={collapseAll}>
505
+ Collapse All
506
+ </Button>
507
+ <span className="text-xs text-muted-foreground ml-auto">
508
+ Total Cost: {formatCost(plan.totalCost)}
509
+ </span>
510
+ </div>
511
+
512
+ {/* Column headers */}
513
+ <div className="flex items-center gap-2 px-2 py-1.5 border-b bg-muted/50 text-xs font-medium text-muted-foreground">
514
+ <div className="flex-1 pl-7">Operation</div>
515
+ <div className="w-24 text-center">Cost</div>
516
+ <div className="w-20 text-right">Rows</div>
517
+ {showActual && <div className="w-20 text-right">Time</div>}
518
+ {showBuffers && <div className="w-24 text-right">Buffers</div>}
519
+ </div>
520
+
521
+ {/* Plan tree */}
522
+ <ScrollArea className="flex-1 max-h-[500px]">
523
+ <ExplainTree
524
+ node={plan.rootNode}
525
+ depth={0}
526
+ maxCost={plan.totalCost}
527
+ expandedIds={expandedIds}
528
+ onToggle={toggleExpand}
529
+ onNodeClick={onNodeClick}
530
+ showActual={showActual}
531
+ showBuffers={showBuffers}
532
+ />
533
+ </ScrollArea>
534
+
535
+ {/* Legend */}
536
+ <div className="p-2 border-t bg-muted/30 text-xs text-muted-foreground">
537
+ <div className="flex items-center gap-4 flex-wrap">
538
+ <span className="flex items-center gap-1">
539
+ <Search className="h-3 w-3 text-green-500" />
540
+ Index Scan (efficient)
541
+ </span>
542
+ <span className="flex items-center gap-1">
543
+ <Table2 className="h-3 w-3 text-red-500" />
544
+ Seq Scan (may need index)
545
+ </span>
546
+ <span className="flex items-center gap-1">
547
+ <Layers className="h-3 w-3 text-blue-500" />
548
+ Join
549
+ </span>
550
+ <span className="flex items-center gap-1">
551
+ <ArrowDownUp className="h-3 w-3 text-orange-500" />
552
+ Sort
553
+ </span>
554
+ </div>
555
+ </div>
556
+ </div>
557
+ )
558
+ }
559
+
560
+ // Default sample query plan for demo
561
+ export const defaultQueryPlan: QueryPlan = {
562
+ query: `SELECT u.name, COUNT(p.id) as post_count
563
+ FROM users u
564
+ LEFT JOIN posts p ON p.user_id = u.id
565
+ WHERE u.created_at > '2024-01-01'
566
+ GROUP BY u.id, u.name
567
+ ORDER BY post_count DESC
568
+ LIMIT 10;`,
569
+ planningTime: 0.125,
570
+ executionTime: 45.32,
571
+ totalCost: 1250.50,
572
+ rootNode: {
573
+ id: "1",
574
+ type: "Limit",
575
+ startupCost: 1250.50,
576
+ totalCost: 1250.50,
577
+ rows: 10,
578
+ actualRows: 10,
579
+ actualTime: 45.32,
580
+ children: [
581
+ {
582
+ id: "2",
583
+ type: "Sort",
584
+ startupCost: 1250.48,
585
+ totalCost: 1250.50,
586
+ rows: 100,
587
+ actualRows: 100,
588
+ actualTime: 45.28,
589
+ sortKey: ["post_count DESC"],
590
+ children: [
591
+ {
592
+ id: "3",
593
+ type: "Aggregate",
594
+ startupCost: 1150.00,
595
+ totalCost: 1240.00,
596
+ rows: 100,
597
+ actualRows: 100,
598
+ actualTime: 42.15,
599
+ children: [
600
+ {
601
+ id: "4",
602
+ type: "Hash Join",
603
+ joinType: "Left",
604
+ startupCost: 50.00,
605
+ totalCost: 1100.00,
606
+ rows: 5000,
607
+ actualRows: 4850,
608
+ actualTime: 38.50,
609
+ children: [
610
+ {
611
+ id: "5",
612
+ type: "Seq Scan",
613
+ relation: "users",
614
+ alias: "u",
615
+ filter: "created_at > '2024-01-01'",
616
+ startupCost: 0,
617
+ totalCost: 25.00,
618
+ rows: 100,
619
+ actualRows: 98,
620
+ actualTime: 1.25,
621
+ warnings: ["Consider adding an index on users(created_at)"],
622
+ },
623
+ {
624
+ id: "6",
625
+ type: "Hash",
626
+ startupCost: 20.00,
627
+ totalCost: 20.00,
628
+ rows: 5000,
629
+ actualRows: 5000,
630
+ actualTime: 15.00,
631
+ children: [
632
+ {
633
+ id: "7",
634
+ type: "Seq Scan",
635
+ relation: "posts",
636
+ alias: "p",
637
+ startupCost: 0,
638
+ totalCost: 15.00,
639
+ rows: 5000,
640
+ actualRows: 5000,
641
+ actualTime: 12.00,
642
+ buffers: {
643
+ sharedHit: 150,
644
+ sharedRead: 25,
645
+ },
646
+ },
647
+ ],
648
+ },
649
+ ],
650
+ },
651
+ ],
652
+ },
653
+ ],
654
+ },
655
+ ],
656
+ },
657
+ }
@@ -53,10 +53,10 @@ export interface WakaQuotaBarProps {
53
53
  // ============================================================================
54
54
 
55
55
  const defaultColors: WakaQuotaBarColors = {
56
- normal: "bg-green-500",
57
- warning: "bg-yellow-500",
58
- danger: "bg-red-500",
59
- overflow: "bg-red-600",
56
+ normal: "bg-success",
57
+ warning: "bg-warning",
58
+ danger: "bg-destructive",
59
+ overflow: "bg-destructive",
60
60
  }
61
61
 
62
62
  // ============================================================================