@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,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
+ }