@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,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-
|
|
57
|
-
warning: "bg-
|
|
58
|
-
danger: "bg-
|
|
59
|
-
overflow: "bg-
|
|
56
|
+
normal: "bg-success",
|
|
57
|
+
warning: "bg-warning",
|
|
58
|
+
danger: "bg-destructive",
|
|
59
|
+
overflow: "bg-destructive",
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
// ============================================================================
|