@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,738 @@
|
|
|
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 { Input } from "../../components/input"
|
|
9
|
+
import { ScrollArea } from "../../components/scroll-area"
|
|
10
|
+
import {
|
|
11
|
+
Select,
|
|
12
|
+
SelectContent,
|
|
13
|
+
SelectItem,
|
|
14
|
+
SelectTrigger,
|
|
15
|
+
SelectValue,
|
|
16
|
+
} from "../../components/select"
|
|
17
|
+
import {
|
|
18
|
+
DropdownMenu,
|
|
19
|
+
DropdownMenuContent,
|
|
20
|
+
DropdownMenuItem,
|
|
21
|
+
DropdownMenuSeparator,
|
|
22
|
+
DropdownMenuTrigger,
|
|
23
|
+
} from "../../components/dropdown-menu"
|
|
24
|
+
import {
|
|
25
|
+
GitBranch,
|
|
26
|
+
Play,
|
|
27
|
+
Pause,
|
|
28
|
+
Square,
|
|
29
|
+
Plus,
|
|
30
|
+
Trash2,
|
|
31
|
+
GripVertical,
|
|
32
|
+
Settings,
|
|
33
|
+
ChevronDown,
|
|
34
|
+
ChevronRight,
|
|
35
|
+
Box,
|
|
36
|
+
Terminal,
|
|
37
|
+
Upload,
|
|
38
|
+
Download,
|
|
39
|
+
Shield,
|
|
40
|
+
TestTube,
|
|
41
|
+
Rocket,
|
|
42
|
+
Container,
|
|
43
|
+
Database,
|
|
44
|
+
Bell,
|
|
45
|
+
CheckCircle2,
|
|
46
|
+
XCircle,
|
|
47
|
+
Clock,
|
|
48
|
+
MoreVertical,
|
|
49
|
+
Copy,
|
|
50
|
+
Eye,
|
|
51
|
+
ArrowRight,
|
|
52
|
+
Zap,
|
|
53
|
+
} from "lucide-react"
|
|
54
|
+
|
|
55
|
+
export type StepType = "build" | "test" | "deploy" | "script" | "docker" | "notify" | "approval" | "artifact" | "cache" | "parallel"
|
|
56
|
+
export type StepStatus = "pending" | "running" | "success" | "failed" | "skipped" | "waiting"
|
|
57
|
+
|
|
58
|
+
export interface PipelineStep {
|
|
59
|
+
id: string
|
|
60
|
+
name: string
|
|
61
|
+
type: StepType
|
|
62
|
+
status?: StepStatus
|
|
63
|
+
command?: string
|
|
64
|
+
image?: string
|
|
65
|
+
environment?: Record<string, string>
|
|
66
|
+
artifacts?: string[]
|
|
67
|
+
dependsOn?: string[]
|
|
68
|
+
timeout?: number
|
|
69
|
+
retries?: number
|
|
70
|
+
condition?: string
|
|
71
|
+
parallel?: PipelineStep[]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface PipelineStage {
|
|
75
|
+
id: string
|
|
76
|
+
name: string
|
|
77
|
+
steps: PipelineStep[]
|
|
78
|
+
status?: StepStatus
|
|
79
|
+
runParallel?: boolean
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface Pipeline {
|
|
83
|
+
id: string
|
|
84
|
+
name: string
|
|
85
|
+
description?: string
|
|
86
|
+
trigger?: {
|
|
87
|
+
branches?: string[]
|
|
88
|
+
events?: ("push" | "pull_request" | "tag" | "schedule")[]
|
|
89
|
+
schedule?: string
|
|
90
|
+
}
|
|
91
|
+
stages: PipelineStage[]
|
|
92
|
+
variables?: Record<string, string>
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface CICDBuilderProps {
|
|
96
|
+
pipeline: Pipeline
|
|
97
|
+
onPipelineChange?: (pipeline: Pipeline) => void
|
|
98
|
+
onRun?: () => void
|
|
99
|
+
onSave?: () => void
|
|
100
|
+
onExport?: (format: "yaml" | "json") => void
|
|
101
|
+
readOnly?: boolean
|
|
102
|
+
className?: string
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const stepTypeConfig: Record<StepType, { icon: React.ElementType; color: string; label: string }> = {
|
|
106
|
+
build: { icon: Box, color: "text-blue-500", label: "Build" },
|
|
107
|
+
test: { icon: TestTube, color: "text-purple-500", label: "Test" },
|
|
108
|
+
deploy: { icon: Rocket, color: "text-green-500", label: "Deploy" },
|
|
109
|
+
script: { icon: Terminal, color: "text-gray-500", label: "Script" },
|
|
110
|
+
docker: { icon: Container, color: "text-cyan-500", label: "Docker" },
|
|
111
|
+
notify: { icon: Bell, color: "text-yellow-500", label: "Notify" },
|
|
112
|
+
approval: { icon: Shield, color: "text-orange-500", label: "Approval" },
|
|
113
|
+
artifact: { icon: Upload, color: "text-indigo-500", label: "Artifact" },
|
|
114
|
+
cache: { icon: Database, color: "text-pink-500", label: "Cache" },
|
|
115
|
+
parallel: { icon: Zap, color: "text-amber-500", label: "Parallel" },
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const statusConfig: Record<StepStatus, { color: string; bgColor: string; icon: React.ElementType }> = {
|
|
119
|
+
pending: { color: "text-muted-foreground", bgColor: "bg-muted", icon: Clock },
|
|
120
|
+
running: { color: "text-blue-500", bgColor: "bg-blue-500", icon: Play },
|
|
121
|
+
success: { color: "text-green-500", bgColor: "bg-green-500", icon: CheckCircle2 },
|
|
122
|
+
failed: { color: "text-red-500", bgColor: "bg-red-500", icon: XCircle },
|
|
123
|
+
skipped: { color: "text-gray-400", bgColor: "bg-gray-400", icon: ChevronRight },
|
|
124
|
+
waiting: { color: "text-yellow-500", bgColor: "bg-yellow-500", icon: Clock },
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function StepEditor({
|
|
128
|
+
step,
|
|
129
|
+
onUpdate,
|
|
130
|
+
onDelete,
|
|
131
|
+
readOnly,
|
|
132
|
+
}: {
|
|
133
|
+
step: PipelineStep
|
|
134
|
+
onUpdate: (step: PipelineStep) => void
|
|
135
|
+
onDelete: () => void
|
|
136
|
+
readOnly?: boolean
|
|
137
|
+
}) {
|
|
138
|
+
const [expanded, setExpanded] = React.useState(false)
|
|
139
|
+
const typeConf = stepTypeConfig[step.type]
|
|
140
|
+
const TypeIcon = typeConf.icon
|
|
141
|
+
const statusConf = step.status ? statusConfig[step.status] : null
|
|
142
|
+
const StatusIcon = statusConf?.icon
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<div className={cn(
|
|
146
|
+
"border rounded-lg transition-all",
|
|
147
|
+
step.status === "failed" && "border-red-500/50",
|
|
148
|
+
step.status === "running" && "border-blue-500/50 bg-blue-500/5"
|
|
149
|
+
)}>
|
|
150
|
+
<div
|
|
151
|
+
className="flex items-center gap-3 p-3 cursor-pointer"
|
|
152
|
+
onClick={() => setExpanded(!expanded)}
|
|
153
|
+
>
|
|
154
|
+
{!readOnly && (
|
|
155
|
+
<GripVertical className="h-4 w-4 text-muted-foreground cursor-grab" />
|
|
156
|
+
)}
|
|
157
|
+
|
|
158
|
+
<div className={cn("p-1.5 rounded", `${typeConf.color.replace("text-", "bg-")}/10`)}>
|
|
159
|
+
<TypeIcon className={cn("h-4 w-4", typeConf.color)} />
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<div className="flex-1 min-w-0">
|
|
163
|
+
<div className="flex items-center gap-2">
|
|
164
|
+
<span className="font-medium">{step.name}</span>
|
|
165
|
+
<Badge variant="outline" className="text-xs">
|
|
166
|
+
{typeConf.label}
|
|
167
|
+
</Badge>
|
|
168
|
+
</div>
|
|
169
|
+
{step.command && (
|
|
170
|
+
<code className="text-xs text-muted-foreground font-mono truncate block mt-0.5">
|
|
171
|
+
{step.command}
|
|
172
|
+
</code>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
{StatusIcon && (
|
|
177
|
+
<Badge variant="outline" className={cn("text-xs", statusConf?.color)}>
|
|
178
|
+
<StatusIcon className="h-3 w-3 mr-1" />
|
|
179
|
+
{step.status}
|
|
180
|
+
</Badge>
|
|
181
|
+
)}
|
|
182
|
+
|
|
183
|
+
{!readOnly && (
|
|
184
|
+
<DropdownMenu>
|
|
185
|
+
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
|
|
186
|
+
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
|
187
|
+
<MoreVertical className="h-4 w-4" />
|
|
188
|
+
</Button>
|
|
189
|
+
</DropdownMenuTrigger>
|
|
190
|
+
<DropdownMenuContent align="end">
|
|
191
|
+
<DropdownMenuItem onClick={() => setExpanded(true)}>
|
|
192
|
+
<Settings className="h-4 w-4 mr-2" />
|
|
193
|
+
Configure
|
|
194
|
+
</DropdownMenuItem>
|
|
195
|
+
<DropdownMenuItem>
|
|
196
|
+
<Copy className="h-4 w-4 mr-2" />
|
|
197
|
+
Duplicate
|
|
198
|
+
</DropdownMenuItem>
|
|
199
|
+
<DropdownMenuSeparator />
|
|
200
|
+
<DropdownMenuItem className="text-destructive" onClick={onDelete}>
|
|
201
|
+
<Trash2 className="h-4 w-4 mr-2" />
|
|
202
|
+
Delete
|
|
203
|
+
</DropdownMenuItem>
|
|
204
|
+
</DropdownMenuContent>
|
|
205
|
+
</DropdownMenu>
|
|
206
|
+
)}
|
|
207
|
+
|
|
208
|
+
<ChevronRight className={cn("h-4 w-4 transition-transform", expanded && "rotate-90")} />
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
{expanded && (
|
|
212
|
+
<div className="px-3 pb-3 pt-0 space-y-3 border-t mt-2">
|
|
213
|
+
<div className="grid grid-cols-2 gap-3">
|
|
214
|
+
<div>
|
|
215
|
+
<label className="text-xs text-muted-foreground">Step Name</label>
|
|
216
|
+
<Input
|
|
217
|
+
value={step.name}
|
|
218
|
+
onChange={(e) => onUpdate({ ...step, name: e.target.value })}
|
|
219
|
+
className="h-8 mt-1"
|
|
220
|
+
disabled={readOnly}
|
|
221
|
+
/>
|
|
222
|
+
</div>
|
|
223
|
+
<div>
|
|
224
|
+
<label className="text-xs text-muted-foreground">Type</label>
|
|
225
|
+
<Select
|
|
226
|
+
value={step.type}
|
|
227
|
+
onValueChange={(v) => onUpdate({ ...step, type: v as StepType })}
|
|
228
|
+
disabled={readOnly}
|
|
229
|
+
>
|
|
230
|
+
<SelectTrigger className="h-8 mt-1">
|
|
231
|
+
<SelectValue />
|
|
232
|
+
</SelectTrigger>
|
|
233
|
+
<SelectContent>
|
|
234
|
+
{Object.entries(stepTypeConfig).map(([type, conf]) => (
|
|
235
|
+
<SelectItem key={type} value={type}>
|
|
236
|
+
<div className="flex items-center gap-2">
|
|
237
|
+
<conf.icon className={cn("h-4 w-4", conf.color)} />
|
|
238
|
+
{conf.label}
|
|
239
|
+
</div>
|
|
240
|
+
</SelectItem>
|
|
241
|
+
))}
|
|
242
|
+
</SelectContent>
|
|
243
|
+
</Select>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
{(step.type === "script" || step.type === "build" || step.type === "test") && (
|
|
248
|
+
<div>
|
|
249
|
+
<label className="text-xs text-muted-foreground">Command</label>
|
|
250
|
+
<Input
|
|
251
|
+
value={step.command || ""}
|
|
252
|
+
onChange={(e) => onUpdate({ ...step, command: e.target.value })}
|
|
253
|
+
placeholder="npm run build"
|
|
254
|
+
className="h-8 mt-1 font-mono text-sm"
|
|
255
|
+
disabled={readOnly}
|
|
256
|
+
/>
|
|
257
|
+
</div>
|
|
258
|
+
)}
|
|
259
|
+
|
|
260
|
+
{step.type === "docker" && (
|
|
261
|
+
<div>
|
|
262
|
+
<label className="text-xs text-muted-foreground">Docker Image</label>
|
|
263
|
+
<Input
|
|
264
|
+
value={step.image || ""}
|
|
265
|
+
onChange={(e) => onUpdate({ ...step, image: e.target.value })}
|
|
266
|
+
placeholder="node:18-alpine"
|
|
267
|
+
className="h-8 mt-1 font-mono text-sm"
|
|
268
|
+
disabled={readOnly}
|
|
269
|
+
/>
|
|
270
|
+
</div>
|
|
271
|
+
)}
|
|
272
|
+
|
|
273
|
+
<div className="grid grid-cols-2 gap-3">
|
|
274
|
+
<div>
|
|
275
|
+
<label className="text-xs text-muted-foreground">Timeout (seconds)</label>
|
|
276
|
+
<Input
|
|
277
|
+
type="number"
|
|
278
|
+
value={step.timeout || ""}
|
|
279
|
+
onChange={(e) => onUpdate({ ...step, timeout: parseInt(e.target.value) || undefined })}
|
|
280
|
+
placeholder="300"
|
|
281
|
+
className="h-8 mt-1"
|
|
282
|
+
disabled={readOnly}
|
|
283
|
+
/>
|
|
284
|
+
</div>
|
|
285
|
+
<div>
|
|
286
|
+
<label className="text-xs text-muted-foreground">Retries</label>
|
|
287
|
+
<Input
|
|
288
|
+
type="number"
|
|
289
|
+
value={step.retries || ""}
|
|
290
|
+
onChange={(e) => onUpdate({ ...step, retries: parseInt(e.target.value) || undefined })}
|
|
291
|
+
placeholder="0"
|
|
292
|
+
className="h-8 mt-1"
|
|
293
|
+
disabled={readOnly}
|
|
294
|
+
/>
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
|
|
298
|
+
{step.condition !== undefined && (
|
|
299
|
+
<div>
|
|
300
|
+
<label className="text-xs text-muted-foreground">Condition</label>
|
|
301
|
+
<Input
|
|
302
|
+
value={step.condition}
|
|
303
|
+
onChange={(e) => onUpdate({ ...step, condition: e.target.value })}
|
|
304
|
+
placeholder="$CI_COMMIT_BRANCH == 'main'"
|
|
305
|
+
className="h-8 mt-1 font-mono text-sm"
|
|
306
|
+
disabled={readOnly}
|
|
307
|
+
/>
|
|
308
|
+
</div>
|
|
309
|
+
)}
|
|
310
|
+
</div>
|
|
311
|
+
)}
|
|
312
|
+
</div>
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function StageEditor({
|
|
317
|
+
stage,
|
|
318
|
+
onUpdate,
|
|
319
|
+
onDelete,
|
|
320
|
+
onAddStep,
|
|
321
|
+
readOnly,
|
|
322
|
+
}: {
|
|
323
|
+
stage: PipelineStage
|
|
324
|
+
onUpdate: (stage: PipelineStage) => void
|
|
325
|
+
onDelete: () => void
|
|
326
|
+
onAddStep: (type: StepType) => void
|
|
327
|
+
readOnly?: boolean
|
|
328
|
+
}) {
|
|
329
|
+
const [collapsed, setCollapsed] = React.useState(false)
|
|
330
|
+
const statusConf = stage.status ? statusConfig[stage.status] : null
|
|
331
|
+
const StatusIcon = statusConf?.icon
|
|
332
|
+
|
|
333
|
+
const updateStep = (stepId: string, updatedStep: PipelineStep) => {
|
|
334
|
+
onUpdate({
|
|
335
|
+
...stage,
|
|
336
|
+
steps: stage.steps.map((s) => (s.id === stepId ? updatedStep : s)),
|
|
337
|
+
})
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const deleteStep = (stepId: string) => {
|
|
341
|
+
onUpdate({
|
|
342
|
+
...stage,
|
|
343
|
+
steps: stage.steps.filter((s) => s.id !== stepId),
|
|
344
|
+
})
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return (
|
|
348
|
+
<Card className={cn(
|
|
349
|
+
stage.status === "failed" && "border-red-500/30",
|
|
350
|
+
stage.status === "running" && "border-blue-500/30"
|
|
351
|
+
)}>
|
|
352
|
+
<CardHeader className="pb-2">
|
|
353
|
+
<div className="flex items-center gap-3">
|
|
354
|
+
<button onClick={() => setCollapsed(!collapsed)}>
|
|
355
|
+
<ChevronDown className={cn("h-4 w-4 transition-transform", collapsed && "-rotate-90")} />
|
|
356
|
+
</button>
|
|
357
|
+
|
|
358
|
+
<Input
|
|
359
|
+
value={stage.name}
|
|
360
|
+
onChange={(e) => onUpdate({ ...stage, name: e.target.value })}
|
|
361
|
+
className="h-8 font-semibold border-0 p-0 focus-visible:ring-0 bg-transparent"
|
|
362
|
+
disabled={readOnly}
|
|
363
|
+
/>
|
|
364
|
+
|
|
365
|
+
{StatusIcon && (
|
|
366
|
+
<Badge variant="outline" className={cn("text-xs ml-auto", statusConf?.color)}>
|
|
367
|
+
<StatusIcon className="h-3 w-3 mr-1" />
|
|
368
|
+
{stage.status}
|
|
369
|
+
</Badge>
|
|
370
|
+
)}
|
|
371
|
+
|
|
372
|
+
<Badge variant="secondary" className="text-xs">
|
|
373
|
+
{stage.steps.length} steps
|
|
374
|
+
</Badge>
|
|
375
|
+
|
|
376
|
+
{!readOnly && (
|
|
377
|
+
<DropdownMenu>
|
|
378
|
+
<DropdownMenuTrigger asChild>
|
|
379
|
+
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
|
380
|
+
<MoreVertical className="h-4 w-4" />
|
|
381
|
+
</Button>
|
|
382
|
+
</DropdownMenuTrigger>
|
|
383
|
+
<DropdownMenuContent align="end">
|
|
384
|
+
<DropdownMenuItem>
|
|
385
|
+
<Copy className="h-4 w-4 mr-2" />
|
|
386
|
+
Duplicate Stage
|
|
387
|
+
</DropdownMenuItem>
|
|
388
|
+
<DropdownMenuSeparator />
|
|
389
|
+
<DropdownMenuItem className="text-destructive" onClick={onDelete}>
|
|
390
|
+
<Trash2 className="h-4 w-4 mr-2" />
|
|
391
|
+
Delete Stage
|
|
392
|
+
</DropdownMenuItem>
|
|
393
|
+
</DropdownMenuContent>
|
|
394
|
+
</DropdownMenu>
|
|
395
|
+
)}
|
|
396
|
+
</div>
|
|
397
|
+
</CardHeader>
|
|
398
|
+
|
|
399
|
+
{!collapsed && (
|
|
400
|
+
<CardContent className="space-y-2">
|
|
401
|
+
{stage.steps.map((step) => (
|
|
402
|
+
<StepEditor
|
|
403
|
+
key={step.id}
|
|
404
|
+
step={step}
|
|
405
|
+
onUpdate={(s) => updateStep(step.id, s)}
|
|
406
|
+
onDelete={() => deleteStep(step.id)}
|
|
407
|
+
readOnly={readOnly}
|
|
408
|
+
/>
|
|
409
|
+
))}
|
|
410
|
+
|
|
411
|
+
{!readOnly && (
|
|
412
|
+
<DropdownMenu>
|
|
413
|
+
<DropdownMenuTrigger asChild>
|
|
414
|
+
<Button variant="outline" size="sm" className="w-full">
|
|
415
|
+
<Plus className="h-4 w-4 mr-1" />
|
|
416
|
+
Add Step
|
|
417
|
+
</Button>
|
|
418
|
+
</DropdownMenuTrigger>
|
|
419
|
+
<DropdownMenuContent className="w-48">
|
|
420
|
+
{Object.entries(stepTypeConfig).map(([type, conf]) => (
|
|
421
|
+
<DropdownMenuItem key={type} onClick={() => onAddStep(type as StepType)}>
|
|
422
|
+
<conf.icon className={cn("h-4 w-4 mr-2", conf.color)} />
|
|
423
|
+
{conf.label}
|
|
424
|
+
</DropdownMenuItem>
|
|
425
|
+
))}
|
|
426
|
+
</DropdownMenuContent>
|
|
427
|
+
</DropdownMenu>
|
|
428
|
+
)}
|
|
429
|
+
</CardContent>
|
|
430
|
+
)}
|
|
431
|
+
</Card>
|
|
432
|
+
)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export function CICDBuilder({
|
|
436
|
+
pipeline: initialPipeline,
|
|
437
|
+
onPipelineChange,
|
|
438
|
+
onRun,
|
|
439
|
+
onSave,
|
|
440
|
+
onExport,
|
|
441
|
+
readOnly = false,
|
|
442
|
+
className,
|
|
443
|
+
}: CICDBuilderProps) {
|
|
444
|
+
const [pipeline, setPipeline] = React.useState(initialPipeline)
|
|
445
|
+
|
|
446
|
+
const updatePipeline = (updates: Partial<Pipeline>) => {
|
|
447
|
+
const updated = { ...pipeline, ...updates }
|
|
448
|
+
setPipeline(updated)
|
|
449
|
+
onPipelineChange?.(updated)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const addStage = () => {
|
|
453
|
+
const newStage: PipelineStage = {
|
|
454
|
+
id: `stage-${Date.now()}`,
|
|
455
|
+
name: `Stage ${pipeline.stages.length + 1}`,
|
|
456
|
+
steps: [],
|
|
457
|
+
}
|
|
458
|
+
updatePipeline({ stages: [...pipeline.stages, newStage] })
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const updateStage = (stageId: string, updatedStage: PipelineStage) => {
|
|
462
|
+
updatePipeline({
|
|
463
|
+
stages: pipeline.stages.map((s) => (s.id === stageId ? updatedStage : s)),
|
|
464
|
+
})
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const deleteStage = (stageId: string) => {
|
|
468
|
+
updatePipeline({
|
|
469
|
+
stages: pipeline.stages.filter((s) => s.id !== stageId),
|
|
470
|
+
})
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const addStep = (stageId: string, type: StepType) => {
|
|
474
|
+
const stage = pipeline.stages.find((s) => s.id === stageId)
|
|
475
|
+
if (!stage) return
|
|
476
|
+
|
|
477
|
+
const newStep: PipelineStep = {
|
|
478
|
+
id: `step-${Date.now()}`,
|
|
479
|
+
name: `${stepTypeConfig[type].label} Step`,
|
|
480
|
+
type,
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
updateStage(stageId, {
|
|
484
|
+
...stage,
|
|
485
|
+
steps: [...stage.steps, newStep],
|
|
486
|
+
})
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return (
|
|
490
|
+
<div className={cn("flex flex-col gap-6", className)}>
|
|
491
|
+
{/* Header */}
|
|
492
|
+
<Card>
|
|
493
|
+
<CardHeader className="pb-3">
|
|
494
|
+
<div className="flex items-center justify-between">
|
|
495
|
+
<div className="flex items-center gap-3">
|
|
496
|
+
<GitBranch className="h-5 w-5" />
|
|
497
|
+
<div>
|
|
498
|
+
<Input
|
|
499
|
+
value={pipeline.name}
|
|
500
|
+
onChange={(e) => updatePipeline({ name: e.target.value })}
|
|
501
|
+
className="h-8 text-lg font-bold border-0 p-0 focus-visible:ring-0 bg-transparent"
|
|
502
|
+
disabled={readOnly}
|
|
503
|
+
/>
|
|
504
|
+
{pipeline.description && (
|
|
505
|
+
<p className="text-sm text-muted-foreground">{pipeline.description}</p>
|
|
506
|
+
)}
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
509
|
+
|
|
510
|
+
<div className="flex items-center gap-2">
|
|
511
|
+
{onExport && (
|
|
512
|
+
<DropdownMenu>
|
|
513
|
+
<DropdownMenuTrigger asChild>
|
|
514
|
+
<Button variant="outline" size="sm">
|
|
515
|
+
<Download className="h-4 w-4 mr-1" />
|
|
516
|
+
Export
|
|
517
|
+
</Button>
|
|
518
|
+
</DropdownMenuTrigger>
|
|
519
|
+
<DropdownMenuContent>
|
|
520
|
+
<DropdownMenuItem onClick={() => onExport("yaml")}>
|
|
521
|
+
Export as YAML
|
|
522
|
+
</DropdownMenuItem>
|
|
523
|
+
<DropdownMenuItem onClick={() => onExport("json")}>
|
|
524
|
+
Export as JSON
|
|
525
|
+
</DropdownMenuItem>
|
|
526
|
+
</DropdownMenuContent>
|
|
527
|
+
</DropdownMenu>
|
|
528
|
+
)}
|
|
529
|
+
{onSave && !readOnly && (
|
|
530
|
+
<Button variant="outline" size="sm" onClick={onSave}>
|
|
531
|
+
Save
|
|
532
|
+
</Button>
|
|
533
|
+
)}
|
|
534
|
+
{onRun && (
|
|
535
|
+
<Button size="sm" onClick={onRun}>
|
|
536
|
+
<Play className="h-4 w-4 mr-1" />
|
|
537
|
+
Run Pipeline
|
|
538
|
+
</Button>
|
|
539
|
+
)}
|
|
540
|
+
</div>
|
|
541
|
+
</div>
|
|
542
|
+
</CardHeader>
|
|
543
|
+
|
|
544
|
+
{/* Triggers */}
|
|
545
|
+
{pipeline.trigger && (
|
|
546
|
+
<CardContent className="pt-0">
|
|
547
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
548
|
+
<span className="text-sm text-muted-foreground">Triggers:</span>
|
|
549
|
+
{pipeline.trigger.events?.map((event) => (
|
|
550
|
+
<Badge key={event} variant="outline" className="text-xs">
|
|
551
|
+
{event}
|
|
552
|
+
</Badge>
|
|
553
|
+
))}
|
|
554
|
+
{pipeline.trigger.branches?.map((branch) => (
|
|
555
|
+
<Badge key={branch} variant="secondary" className="text-xs">
|
|
556
|
+
<GitBranch className="h-3 w-3 mr-1" />
|
|
557
|
+
{branch}
|
|
558
|
+
</Badge>
|
|
559
|
+
))}
|
|
560
|
+
{pipeline.trigger.schedule && (
|
|
561
|
+
<Badge variant="outline" className="text-xs">
|
|
562
|
+
<Clock className="h-3 w-3 mr-1" />
|
|
563
|
+
{pipeline.trigger.schedule}
|
|
564
|
+
</Badge>
|
|
565
|
+
)}
|
|
566
|
+
</div>
|
|
567
|
+
</CardContent>
|
|
568
|
+
)}
|
|
569
|
+
</Card>
|
|
570
|
+
|
|
571
|
+
{/* Pipeline visualization */}
|
|
572
|
+
<div className="flex items-center gap-2 overflow-x-auto pb-2">
|
|
573
|
+
{pipeline.stages.map((stage, index) => (
|
|
574
|
+
<React.Fragment key={stage.id}>
|
|
575
|
+
<div className="flex flex-col items-center gap-1">
|
|
576
|
+
<div className={cn(
|
|
577
|
+
"w-4 h-4 rounded-full border-2",
|
|
578
|
+
stage.status === "success" && "bg-green-500 border-green-500",
|
|
579
|
+
stage.status === "failed" && "bg-red-500 border-red-500",
|
|
580
|
+
stage.status === "running" && "bg-blue-500 border-blue-500 animate-pulse",
|
|
581
|
+
!stage.status && "bg-muted border-muted-foreground"
|
|
582
|
+
)} />
|
|
583
|
+
<span className="text-xs text-muted-foreground whitespace-nowrap">{stage.name}</span>
|
|
584
|
+
</div>
|
|
585
|
+
{index < pipeline.stages.length - 1 && (
|
|
586
|
+
<ArrowRight className="h-4 w-4 text-muted-foreground shrink-0" />
|
|
587
|
+
)}
|
|
588
|
+
</React.Fragment>
|
|
589
|
+
))}
|
|
590
|
+
</div>
|
|
591
|
+
|
|
592
|
+
{/* Stages */}
|
|
593
|
+
<ScrollArea className="h-[500px]">
|
|
594
|
+
<div className="space-y-4 pr-4">
|
|
595
|
+
{pipeline.stages.map((stage) => (
|
|
596
|
+
<StageEditor
|
|
597
|
+
key={stage.id}
|
|
598
|
+
stage={stage}
|
|
599
|
+
onUpdate={(s) => updateStage(stage.id, s)}
|
|
600
|
+
onDelete={() => deleteStage(stage.id)}
|
|
601
|
+
onAddStep={(type) => addStep(stage.id, type)}
|
|
602
|
+
readOnly={readOnly}
|
|
603
|
+
/>
|
|
604
|
+
))}
|
|
605
|
+
|
|
606
|
+
{!readOnly && (
|
|
607
|
+
<Button variant="outline" className="w-full" onClick={addStage}>
|
|
608
|
+
<Plus className="h-4 w-4 mr-1" />
|
|
609
|
+
Add Stage
|
|
610
|
+
</Button>
|
|
611
|
+
)}
|
|
612
|
+
</div>
|
|
613
|
+
</ScrollArea>
|
|
614
|
+
</div>
|
|
615
|
+
)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Default sample pipeline
|
|
619
|
+
export const defaultPipeline: Pipeline = {
|
|
620
|
+
id: "1",
|
|
621
|
+
name: "Production Deploy",
|
|
622
|
+
description: "Build, test, and deploy to production",
|
|
623
|
+
trigger: {
|
|
624
|
+
branches: ["main", "release/*"],
|
|
625
|
+
events: ["push", "pull_request"],
|
|
626
|
+
},
|
|
627
|
+
stages: [
|
|
628
|
+
{
|
|
629
|
+
id: "build",
|
|
630
|
+
name: "Build",
|
|
631
|
+
status: "success",
|
|
632
|
+
steps: [
|
|
633
|
+
{
|
|
634
|
+
id: "install",
|
|
635
|
+
name: "Install Dependencies",
|
|
636
|
+
type: "script",
|
|
637
|
+
command: "npm ci",
|
|
638
|
+
status: "success",
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
id: "build",
|
|
642
|
+
name: "Build Application",
|
|
643
|
+
type: "build",
|
|
644
|
+
command: "npm run build",
|
|
645
|
+
status: "success",
|
|
646
|
+
},
|
|
647
|
+
{
|
|
648
|
+
id: "artifact",
|
|
649
|
+
name: "Upload Build Artifacts",
|
|
650
|
+
type: "artifact",
|
|
651
|
+
artifacts: ["dist/**", "build/**"],
|
|
652
|
+
status: "success",
|
|
653
|
+
},
|
|
654
|
+
],
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
id: "test",
|
|
658
|
+
name: "Test",
|
|
659
|
+
status: "running",
|
|
660
|
+
steps: [
|
|
661
|
+
{
|
|
662
|
+
id: "unit",
|
|
663
|
+
name: "Unit Tests",
|
|
664
|
+
type: "test",
|
|
665
|
+
command: "npm run test:unit",
|
|
666
|
+
status: "success",
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
id: "integration",
|
|
670
|
+
name: "Integration Tests",
|
|
671
|
+
type: "test",
|
|
672
|
+
command: "npm run test:integration",
|
|
673
|
+
status: "running",
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
id: "e2e",
|
|
677
|
+
name: "E2E Tests",
|
|
678
|
+
type: "test",
|
|
679
|
+
command: "npm run test:e2e",
|
|
680
|
+
status: "pending",
|
|
681
|
+
},
|
|
682
|
+
],
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
id: "security",
|
|
686
|
+
name: "Security",
|
|
687
|
+
status: "pending",
|
|
688
|
+
steps: [
|
|
689
|
+
{
|
|
690
|
+
id: "sast",
|
|
691
|
+
name: "SAST Scan",
|
|
692
|
+
type: "script",
|
|
693
|
+
command: "npm run security:sast",
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
id: "deps",
|
|
697
|
+
name: "Dependency Check",
|
|
698
|
+
type: "script",
|
|
699
|
+
command: "npm audit",
|
|
700
|
+
},
|
|
701
|
+
],
|
|
702
|
+
},
|
|
703
|
+
{
|
|
704
|
+
id: "deploy",
|
|
705
|
+
name: "Deploy",
|
|
706
|
+
status: "pending",
|
|
707
|
+
steps: [
|
|
708
|
+
{
|
|
709
|
+
id: "approval",
|
|
710
|
+
name: "Production Approval",
|
|
711
|
+
type: "approval",
|
|
712
|
+
condition: "$CI_COMMIT_BRANCH == 'main'",
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
id: "docker",
|
|
716
|
+
name: "Build Docker Image",
|
|
717
|
+
type: "docker",
|
|
718
|
+
image: "app:$CI_COMMIT_SHA",
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
id: "deploy-prod",
|
|
722
|
+
name: "Deploy to Production",
|
|
723
|
+
type: "deploy",
|
|
724
|
+
command: "kubectl apply -f k8s/",
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
id: "notify",
|
|
728
|
+
name: "Notify Team",
|
|
729
|
+
type: "notify",
|
|
730
|
+
},
|
|
731
|
+
],
|
|
732
|
+
},
|
|
733
|
+
],
|
|
734
|
+
variables: {
|
|
735
|
+
NODE_ENV: "production",
|
|
736
|
+
REGISTRY: "gcr.io/my-project",
|
|
737
|
+
},
|
|
738
|
+
}
|