@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,487 @@
|
|
|
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
|
+
DropdownMenu,
|
|
16
|
+
DropdownMenuContent,
|
|
17
|
+
DropdownMenuItem,
|
|
18
|
+
DropdownMenuSeparator,
|
|
19
|
+
DropdownMenuTrigger,
|
|
20
|
+
} from "../dropdown-menu"
|
|
21
|
+
import {
|
|
22
|
+
Database,
|
|
23
|
+
ArrowUp,
|
|
24
|
+
ArrowDown,
|
|
25
|
+
RotateCcw,
|
|
26
|
+
CheckCircle2,
|
|
27
|
+
XCircle,
|
|
28
|
+
Clock,
|
|
29
|
+
Play,
|
|
30
|
+
Pause,
|
|
31
|
+
AlertTriangle,
|
|
32
|
+
MoreVertical,
|
|
33
|
+
FileCode,
|
|
34
|
+
Calendar,
|
|
35
|
+
User,
|
|
36
|
+
Timer,
|
|
37
|
+
ChevronDown,
|
|
38
|
+
ChevronRight,
|
|
39
|
+
} from "lucide-react"
|
|
40
|
+
|
|
41
|
+
export type MigrationStatus = "pending" | "applied" | "failed" | "rolled_back" | "running"
|
|
42
|
+
|
|
43
|
+
export interface Migration {
|
|
44
|
+
id: string
|
|
45
|
+
version: string
|
|
46
|
+
name: string
|
|
47
|
+
description?: string
|
|
48
|
+
status: MigrationStatus
|
|
49
|
+
appliedAt?: Date
|
|
50
|
+
rolledBackAt?: Date
|
|
51
|
+
duration?: number // in ms
|
|
52
|
+
author?: string
|
|
53
|
+
checksum?: string
|
|
54
|
+
upScript?: string
|
|
55
|
+
downScript?: string
|
|
56
|
+
error?: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface WakaMigrationListProps {
|
|
60
|
+
/** List of migrations */
|
|
61
|
+
migrations: Migration[]
|
|
62
|
+
/** Callback when applying a migration */
|
|
63
|
+
onApply?: (migration: Migration) => void
|
|
64
|
+
/** Callback when rolling back a migration */
|
|
65
|
+
onRollback?: (migration: Migration) => void
|
|
66
|
+
/** Callback when viewing migration details */
|
|
67
|
+
onViewDetails?: (migration: Migration) => void
|
|
68
|
+
/** Current database version */
|
|
69
|
+
currentVersion?: string
|
|
70
|
+
/** Show scripts */
|
|
71
|
+
showScripts?: boolean
|
|
72
|
+
/** Title */
|
|
73
|
+
title?: string
|
|
74
|
+
/** Custom class name */
|
|
75
|
+
className?: string
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const statusConfig: Record<MigrationStatus, { label: string; icon: React.ElementType; color: string; bgColor: string }> = {
|
|
79
|
+
pending: { label: "Pending", icon: Clock, color: "text-muted-foreground", bgColor: "bg-muted" },
|
|
80
|
+
applied: { label: "Applied", icon: CheckCircle2, color: "text-green-500", bgColor: "bg-green-500/10" },
|
|
81
|
+
failed: { label: "Failed", icon: XCircle, color: "text-red-500", bgColor: "bg-red-500/10" },
|
|
82
|
+
rolled_back: { label: "Rolled Back", icon: RotateCcw, color: "text-yellow-500", bgColor: "bg-yellow-500/10" },
|
|
83
|
+
running: { label: "Running", icon: Play, color: "text-blue-500", bgColor: "bg-blue-500/10" },
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function formatDate(date: Date): string {
|
|
87
|
+
return date.toLocaleDateString("fr-FR", {
|
|
88
|
+
day: "2-digit",
|
|
89
|
+
month: "short",
|
|
90
|
+
year: "numeric",
|
|
91
|
+
hour: "2-digit",
|
|
92
|
+
minute: "2-digit",
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function formatDuration(ms: number): string {
|
|
97
|
+
if (ms < 1000) return `${ms}ms`
|
|
98
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`
|
|
99
|
+
return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function MigrationRow({
|
|
103
|
+
migration,
|
|
104
|
+
isCurrentVersion,
|
|
105
|
+
onApply,
|
|
106
|
+
onRollback,
|
|
107
|
+
onViewDetails,
|
|
108
|
+
showScripts,
|
|
109
|
+
}: {
|
|
110
|
+
migration: Migration
|
|
111
|
+
isCurrentVersion: boolean
|
|
112
|
+
onApply?: (migration: Migration) => void
|
|
113
|
+
onRollback?: (migration: Migration) => void
|
|
114
|
+
onViewDetails?: (migration: Migration) => void
|
|
115
|
+
showScripts?: boolean
|
|
116
|
+
}) {
|
|
117
|
+
const [expanded, setExpanded] = React.useState(false)
|
|
118
|
+
const statusConf = statusConfig[migration.status]
|
|
119
|
+
const StatusIcon = statusConf.icon
|
|
120
|
+
|
|
121
|
+
const canApply = migration.status === "pending" || migration.status === "rolled_back"
|
|
122
|
+
const canRollback = migration.status === "applied"
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<div className={cn(
|
|
126
|
+
"border-b last:border-b-0",
|
|
127
|
+
isCurrentVersion && "bg-primary/5 border-primary/30",
|
|
128
|
+
migration.status === "failed" && "bg-red-500/5"
|
|
129
|
+
)}>
|
|
130
|
+
<div className="flex items-center gap-3 p-3">
|
|
131
|
+
{/* Expand button */}
|
|
132
|
+
{showScripts && (migration.upScript || migration.downScript) && (
|
|
133
|
+
<button
|
|
134
|
+
className="text-muted-foreground hover:text-foreground transition-colors"
|
|
135
|
+
onClick={() => setExpanded(!expanded)}
|
|
136
|
+
>
|
|
137
|
+
{expanded ? (
|
|
138
|
+
<ChevronDown className="h-4 w-4" />
|
|
139
|
+
) : (
|
|
140
|
+
<ChevronRight className="h-4 w-4" />
|
|
141
|
+
)}
|
|
142
|
+
</button>
|
|
143
|
+
)}
|
|
144
|
+
|
|
145
|
+
{/* Status icon */}
|
|
146
|
+
<div className={cn("p-1.5 rounded", statusConf.bgColor)}>
|
|
147
|
+
<StatusIcon className={cn("h-4 w-4", statusConf.color)} />
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
{/* Version and name */}
|
|
151
|
+
<div className="flex-1 min-w-0">
|
|
152
|
+
<div className="flex items-center gap-2">
|
|
153
|
+
<code className="font-semibold text-sm">{migration.version}</code>
|
|
154
|
+
{isCurrentVersion && (
|
|
155
|
+
<Badge className="bg-primary text-xs">Current</Badge>
|
|
156
|
+
)}
|
|
157
|
+
<span className="font-medium truncate">{migration.name}</span>
|
|
158
|
+
</div>
|
|
159
|
+
{migration.description && (
|
|
160
|
+
<p className="text-sm text-muted-foreground truncate mt-0.5">
|
|
161
|
+
{migration.description}
|
|
162
|
+
</p>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
{/* Metadata */}
|
|
167
|
+
<div className="flex items-center gap-4 text-xs text-muted-foreground shrink-0">
|
|
168
|
+
{migration.author && (
|
|
169
|
+
<span className="flex items-center gap-1">
|
|
170
|
+
<User className="h-3 w-3" />
|
|
171
|
+
{migration.author}
|
|
172
|
+
</span>
|
|
173
|
+
)}
|
|
174
|
+
{migration.appliedAt && (
|
|
175
|
+
<span className="flex items-center gap-1">
|
|
176
|
+
<Calendar className="h-3 w-3" />
|
|
177
|
+
{formatDate(migration.appliedAt)}
|
|
178
|
+
</span>
|
|
179
|
+
)}
|
|
180
|
+
{migration.duration !== undefined && (
|
|
181
|
+
<span className="flex items-center gap-1">
|
|
182
|
+
<Timer className="h-3 w-3" />
|
|
183
|
+
{formatDuration(migration.duration)}
|
|
184
|
+
</span>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
{/* Status badge */}
|
|
189
|
+
<Badge variant="outline" className={cn("shrink-0", statusConf.color)}>
|
|
190
|
+
{statusConf.label}
|
|
191
|
+
</Badge>
|
|
192
|
+
|
|
193
|
+
{/* Actions */}
|
|
194
|
+
<DropdownMenu>
|
|
195
|
+
<DropdownMenuTrigger asChild>
|
|
196
|
+
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
|
197
|
+
<MoreVertical className="h-4 w-4" />
|
|
198
|
+
</Button>
|
|
199
|
+
</DropdownMenuTrigger>
|
|
200
|
+
<DropdownMenuContent align="end">
|
|
201
|
+
{onViewDetails && (
|
|
202
|
+
<DropdownMenuItem onClick={() => onViewDetails(migration)}>
|
|
203
|
+
<FileCode className="h-4 w-4 mr-2" />
|
|
204
|
+
View Details
|
|
205
|
+
</DropdownMenuItem>
|
|
206
|
+
)}
|
|
207
|
+
{onApply && canApply && (
|
|
208
|
+
<DropdownMenuItem onClick={() => onApply(migration)}>
|
|
209
|
+
<ArrowUp className="h-4 w-4 mr-2" />
|
|
210
|
+
Apply Migration
|
|
211
|
+
</DropdownMenuItem>
|
|
212
|
+
)}
|
|
213
|
+
{onRollback && canRollback && (
|
|
214
|
+
<>
|
|
215
|
+
<DropdownMenuSeparator />
|
|
216
|
+
<DropdownMenuItem
|
|
217
|
+
onClick={() => onRollback(migration)}
|
|
218
|
+
className="text-destructive"
|
|
219
|
+
>
|
|
220
|
+
<ArrowDown className="h-4 w-4 mr-2" />
|
|
221
|
+
Rollback
|
|
222
|
+
</DropdownMenuItem>
|
|
223
|
+
</>
|
|
224
|
+
)}
|
|
225
|
+
</DropdownMenuContent>
|
|
226
|
+
</DropdownMenu>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
{/* Error message */}
|
|
230
|
+
{migration.status === "failed" && migration.error && (
|
|
231
|
+
<div className="px-3 pb-3">
|
|
232
|
+
<div className="flex items-start gap-2 p-2 bg-red-500/10 rounded text-sm text-red-500">
|
|
233
|
+
<AlertTriangle className="h-4 w-4 shrink-0 mt-0.5" />
|
|
234
|
+
<pre className="whitespace-pre-wrap font-mono text-xs">{migration.error}</pre>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
{/* Scripts */}
|
|
240
|
+
{showScripts && expanded && (
|
|
241
|
+
<div className="px-3 pb-3 space-y-3">
|
|
242
|
+
{migration.upScript && (
|
|
243
|
+
<div>
|
|
244
|
+
<div className="flex items-center gap-2 text-xs text-muted-foreground mb-1">
|
|
245
|
+
<ArrowUp className="h-3 w-3" />
|
|
246
|
+
Up Migration
|
|
247
|
+
</div>
|
|
248
|
+
<pre className="p-2 bg-muted rounded text-xs font-mono overflow-x-auto">
|
|
249
|
+
{migration.upScript}
|
|
250
|
+
</pre>
|
|
251
|
+
</div>
|
|
252
|
+
)}
|
|
253
|
+
{migration.downScript && (
|
|
254
|
+
<div>
|
|
255
|
+
<div className="flex items-center gap-2 text-xs text-muted-foreground mb-1">
|
|
256
|
+
<ArrowDown className="h-3 w-3" />
|
|
257
|
+
Down Migration (Rollback)
|
|
258
|
+
</div>
|
|
259
|
+
<pre className="p-2 bg-muted rounded text-xs font-mono overflow-x-auto">
|
|
260
|
+
{migration.downScript}
|
|
261
|
+
</pre>
|
|
262
|
+
</div>
|
|
263
|
+
)}
|
|
264
|
+
</div>
|
|
265
|
+
)}
|
|
266
|
+
</div>
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function WakaMigrationList({
|
|
271
|
+
migrations,
|
|
272
|
+
onApply,
|
|
273
|
+
onRollback,
|
|
274
|
+
onViewDetails,
|
|
275
|
+
currentVersion,
|
|
276
|
+
showScripts = false,
|
|
277
|
+
title = "Migrations",
|
|
278
|
+
className,
|
|
279
|
+
}: WakaMigrationListProps) {
|
|
280
|
+
const [filter, setFilter] = React.useState<MigrationStatus | "all">("all")
|
|
281
|
+
|
|
282
|
+
// Calculate stats
|
|
283
|
+
const stats = React.useMemo(() => {
|
|
284
|
+
return {
|
|
285
|
+
total: migrations.length,
|
|
286
|
+
pending: migrations.filter((m) => m.status === "pending").length,
|
|
287
|
+
applied: migrations.filter((m) => m.status === "applied").length,
|
|
288
|
+
failed: migrations.filter((m) => m.status === "failed").length,
|
|
289
|
+
}
|
|
290
|
+
}, [migrations])
|
|
291
|
+
|
|
292
|
+
// Filter migrations
|
|
293
|
+
const filteredMigrations = React.useMemo(() => {
|
|
294
|
+
if (filter === "all") return migrations
|
|
295
|
+
return migrations.filter((m) => m.status === filter)
|
|
296
|
+
}, [migrations, filter])
|
|
297
|
+
|
|
298
|
+
// Sort by version (newest first)
|
|
299
|
+
const sortedMigrations = React.useMemo(() => {
|
|
300
|
+
return [...filteredMigrations].sort((a, b) => b.version.localeCompare(a.version))
|
|
301
|
+
}, [filteredMigrations])
|
|
302
|
+
|
|
303
|
+
return (
|
|
304
|
+
<div className={cn("flex flex-col border rounded-lg bg-background", className)}>
|
|
305
|
+
{/* Header */}
|
|
306
|
+
<div className="flex items-center justify-between gap-4 p-3 border-b">
|
|
307
|
+
<div className="flex items-center gap-3">
|
|
308
|
+
<Database className="h-5 w-5" />
|
|
309
|
+
<h3 className="font-semibold">{title}</h3>
|
|
310
|
+
<Badge variant="secondary">{stats.total}</Badge>
|
|
311
|
+
{currentVersion && (
|
|
312
|
+
<Badge variant="outline" className="text-xs">
|
|
313
|
+
v{currentVersion}
|
|
314
|
+
</Badge>
|
|
315
|
+
)}
|
|
316
|
+
</div>
|
|
317
|
+
|
|
318
|
+
<div className="flex items-center gap-2">
|
|
319
|
+
{stats.pending > 0 && (
|
|
320
|
+
<Badge variant="outline">{stats.pending} pending</Badge>
|
|
321
|
+
)}
|
|
322
|
+
{stats.failed > 0 && (
|
|
323
|
+
<Badge className="bg-red-500">{stats.failed} failed</Badge>
|
|
324
|
+
)}
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
|
|
328
|
+
{/* Filter toolbar */}
|
|
329
|
+
<div className="flex items-center gap-2 p-2 border-b bg-muted/30">
|
|
330
|
+
<span className="text-xs text-muted-foreground">Filter:</span>
|
|
331
|
+
{(["all", "pending", "applied", "failed", "rolled_back"] as const).map((status) => (
|
|
332
|
+
<Button
|
|
333
|
+
key={status}
|
|
334
|
+
variant={filter === status ? "default" : "ghost"}
|
|
335
|
+
size="sm"
|
|
336
|
+
className="h-7 text-xs"
|
|
337
|
+
onClick={() => setFilter(status)}
|
|
338
|
+
>
|
|
339
|
+
{status === "all" ? "All" : statusConfig[status].label}
|
|
340
|
+
</Button>
|
|
341
|
+
))}
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
{/* Migration list */}
|
|
345
|
+
<ScrollArea className="flex-1 max-h-[500px]">
|
|
346
|
+
{sortedMigrations.length === 0 ? (
|
|
347
|
+
<div className="flex flex-col items-center justify-center h-32 text-muted-foreground">
|
|
348
|
+
<Database className="h-8 w-8 mb-2" />
|
|
349
|
+
<span>No migrations found</span>
|
|
350
|
+
</div>
|
|
351
|
+
) : (
|
|
352
|
+
sortedMigrations.map((migration) => (
|
|
353
|
+
<MigrationRow
|
|
354
|
+
key={migration.id}
|
|
355
|
+
migration={migration}
|
|
356
|
+
isCurrentVersion={migration.version === currentVersion}
|
|
357
|
+
onApply={onApply}
|
|
358
|
+
onRollback={onRollback}
|
|
359
|
+
onViewDetails={onViewDetails}
|
|
360
|
+
showScripts={showScripts}
|
|
361
|
+
/>
|
|
362
|
+
))
|
|
363
|
+
)}
|
|
364
|
+
</ScrollArea>
|
|
365
|
+
|
|
366
|
+
{/* Footer */}
|
|
367
|
+
{stats.pending > 0 && onApply && (
|
|
368
|
+
<div className="p-3 border-t bg-muted/30">
|
|
369
|
+
<Button
|
|
370
|
+
size="sm"
|
|
371
|
+
onClick={() => {
|
|
372
|
+
const pending = migrations.filter((m) => m.status === "pending")
|
|
373
|
+
pending.forEach((m) => onApply(m))
|
|
374
|
+
}}
|
|
375
|
+
>
|
|
376
|
+
<ArrowUp className="h-4 w-4 mr-1" />
|
|
377
|
+
Apply All Pending ({stats.pending})
|
|
378
|
+
</Button>
|
|
379
|
+
</div>
|
|
380
|
+
)}
|
|
381
|
+
</div>
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Default sample migrations for demo
|
|
386
|
+
export const defaultMigrations: Migration[] = [
|
|
387
|
+
{
|
|
388
|
+
id: "1",
|
|
389
|
+
version: "20240115_001",
|
|
390
|
+
name: "create_users_table",
|
|
391
|
+
description: "Create users table with basic fields",
|
|
392
|
+
status: "applied",
|
|
393
|
+
appliedAt: new Date(Date.now() - 30 * 24 * 3600000),
|
|
394
|
+
duration: 125,
|
|
395
|
+
author: "john.doe",
|
|
396
|
+
upScript: `CREATE TABLE users (
|
|
397
|
+
id SERIAL PRIMARY KEY,
|
|
398
|
+
email VARCHAR(255) UNIQUE NOT NULL,
|
|
399
|
+
name VARCHAR(255),
|
|
400
|
+
created_at TIMESTAMP DEFAULT NOW()
|
|
401
|
+
);`,
|
|
402
|
+
downScript: "DROP TABLE users;",
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
id: "2",
|
|
406
|
+
version: "20240120_001",
|
|
407
|
+
name: "add_users_password",
|
|
408
|
+
description: "Add password hash column to users",
|
|
409
|
+
status: "applied",
|
|
410
|
+
appliedAt: new Date(Date.now() - 25 * 24 * 3600000),
|
|
411
|
+
duration: 45,
|
|
412
|
+
author: "jane.smith",
|
|
413
|
+
upScript: "ALTER TABLE users ADD COLUMN password_hash VARCHAR(255);",
|
|
414
|
+
downScript: "ALTER TABLE users DROP COLUMN password_hash;",
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
id: "3",
|
|
418
|
+
version: "20240125_001",
|
|
419
|
+
name: "create_posts_table",
|
|
420
|
+
description: "Create posts table with foreign key to users",
|
|
421
|
+
status: "applied",
|
|
422
|
+
appliedAt: new Date(Date.now() - 20 * 24 * 3600000),
|
|
423
|
+
duration: 230,
|
|
424
|
+
author: "john.doe",
|
|
425
|
+
upScript: `CREATE TABLE posts (
|
|
426
|
+
id SERIAL PRIMARY KEY,
|
|
427
|
+
user_id INTEGER REFERENCES users(id),
|
|
428
|
+
title VARCHAR(255) NOT NULL,
|
|
429
|
+
content TEXT,
|
|
430
|
+
created_at TIMESTAMP DEFAULT NOW()
|
|
431
|
+
);
|
|
432
|
+
CREATE INDEX idx_posts_user_id ON posts(user_id);`,
|
|
433
|
+
downScript: `DROP INDEX idx_posts_user_id;
|
|
434
|
+
DROP TABLE posts;`,
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
id: "4",
|
|
438
|
+
version: "20240130_001",
|
|
439
|
+
name: "add_posts_status",
|
|
440
|
+
description: "Add status enum to posts",
|
|
441
|
+
status: "failed",
|
|
442
|
+
author: "jane.smith",
|
|
443
|
+
error: "ERROR: type \"post_status\" already exists\n at migration.up (migrations/20240130_001.ts:5:3)",
|
|
444
|
+
upScript: `CREATE TYPE post_status AS ENUM ('draft', 'published', 'archived');
|
|
445
|
+
ALTER TABLE posts ADD COLUMN status post_status DEFAULT 'draft';`,
|
|
446
|
+
downScript: `ALTER TABLE posts DROP COLUMN status;
|
|
447
|
+
DROP TYPE post_status;`,
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
id: "5",
|
|
451
|
+
version: "20240205_001",
|
|
452
|
+
name: "create_comments_table",
|
|
453
|
+
description: "Create comments table",
|
|
454
|
+
status: "pending",
|
|
455
|
+
author: "john.doe",
|
|
456
|
+
upScript: `CREATE TABLE comments (
|
|
457
|
+
id SERIAL PRIMARY KEY,
|
|
458
|
+
post_id INTEGER REFERENCES posts(id),
|
|
459
|
+
user_id INTEGER REFERENCES users(id),
|
|
460
|
+
content TEXT NOT NULL,
|
|
461
|
+
created_at TIMESTAMP DEFAULT NOW()
|
|
462
|
+
);`,
|
|
463
|
+
downScript: "DROP TABLE comments;",
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
id: "6",
|
|
467
|
+
version: "20240210_001",
|
|
468
|
+
name: "add_full_text_search",
|
|
469
|
+
description: "Add full-text search to posts",
|
|
470
|
+
status: "pending",
|
|
471
|
+
author: "jane.smith",
|
|
472
|
+
upScript: `ALTER TABLE posts ADD COLUMN search_vector tsvector;
|
|
473
|
+
CREATE INDEX idx_posts_search ON posts USING GIN(search_vector);
|
|
474
|
+
CREATE FUNCTION update_posts_search() RETURNS trigger AS $$
|
|
475
|
+
BEGIN
|
|
476
|
+
NEW.search_vector := to_tsvector('english', coalesce(NEW.title, '') || ' ' || coalesce(NEW.content, ''));
|
|
477
|
+
RETURN NEW;
|
|
478
|
+
END;
|
|
479
|
+
$$ LANGUAGE plpgsql;
|
|
480
|
+
CREATE TRIGGER posts_search_update BEFORE INSERT OR UPDATE ON posts
|
|
481
|
+
FOR EACH ROW EXECUTE FUNCTION update_posts_search();`,
|
|
482
|
+
downScript: `DROP TRIGGER posts_search_update ON posts;
|
|
483
|
+
DROP FUNCTION update_posts_search;
|
|
484
|
+
DROP INDEX idx_posts_search;
|
|
485
|
+
ALTER TABLE posts DROP COLUMN search_vector;`,
|
|
486
|
+
},
|
|
487
|
+
]
|