@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,417 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "../../utils/cn"
|
|
5
|
+
import { Button } from "../button"
|
|
6
|
+
import { Input } from "../input"
|
|
7
|
+
import { Badge } from "../badge"
|
|
8
|
+
import { ScrollArea } from "../scroll-area"
|
|
9
|
+
import {
|
|
10
|
+
Tooltip,
|
|
11
|
+
TooltipContent,
|
|
12
|
+
TooltipProvider,
|
|
13
|
+
TooltipTrigger,
|
|
14
|
+
} from "../tooltip"
|
|
15
|
+
import {
|
|
16
|
+
Variable,
|
|
17
|
+
Plus,
|
|
18
|
+
Trash2,
|
|
19
|
+
Eye,
|
|
20
|
+
EyeOff,
|
|
21
|
+
Copy,
|
|
22
|
+
CheckCircle2,
|
|
23
|
+
Search,
|
|
24
|
+
Lock,
|
|
25
|
+
Unlock,
|
|
26
|
+
AlertTriangle,
|
|
27
|
+
Download,
|
|
28
|
+
Upload,
|
|
29
|
+
Save,
|
|
30
|
+
} from "lucide-react"
|
|
31
|
+
|
|
32
|
+
export interface EnvVariable {
|
|
33
|
+
key: string
|
|
34
|
+
value: string
|
|
35
|
+
isSecret?: boolean
|
|
36
|
+
isRequired?: boolean
|
|
37
|
+
description?: string
|
|
38
|
+
source?: string // e.g., "production", "staging", "local"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface WakaEnvVarEditorProps {
|
|
42
|
+
/** List of environment variables */
|
|
43
|
+
variables: EnvVariable[]
|
|
44
|
+
/** Callback when variables change */
|
|
45
|
+
onChange?: (variables: EnvVariable[]) => void
|
|
46
|
+
/** Callback when saving */
|
|
47
|
+
onSave?: (variables: EnvVariable[]) => void
|
|
48
|
+
/** Callback when importing */
|
|
49
|
+
onImport?: (content: string) => void
|
|
50
|
+
/** Callback when exporting */
|
|
51
|
+
onExport?: () => void
|
|
52
|
+
/** Read-only mode */
|
|
53
|
+
readOnly?: boolean
|
|
54
|
+
/** Show source column */
|
|
55
|
+
showSource?: boolean
|
|
56
|
+
/** Title */
|
|
57
|
+
title?: string
|
|
58
|
+
/** Custom class name */
|
|
59
|
+
className?: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function EnvVarRow({
|
|
63
|
+
variable,
|
|
64
|
+
index,
|
|
65
|
+
onChange,
|
|
66
|
+
onDelete,
|
|
67
|
+
readOnly,
|
|
68
|
+
showSource,
|
|
69
|
+
}: {
|
|
70
|
+
variable: EnvVariable
|
|
71
|
+
index: number
|
|
72
|
+
onChange: (index: number, field: keyof EnvVariable, value: string | boolean) => void
|
|
73
|
+
onDelete: (index: number) => void
|
|
74
|
+
readOnly: boolean
|
|
75
|
+
showSource: boolean
|
|
76
|
+
}) {
|
|
77
|
+
const [showValue, setShowValue] = React.useState(!variable.isSecret)
|
|
78
|
+
const [copied, setCopied] = React.useState(false)
|
|
79
|
+
|
|
80
|
+
const copyValue = () => {
|
|
81
|
+
navigator.clipboard.writeText(variable.value)
|
|
82
|
+
setCopied(true)
|
|
83
|
+
setTimeout(() => setCopied(false), 2000)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const maskedValue = variable.isSecret && !showValue
|
|
87
|
+
? "•".repeat(Math.min(variable.value.length, 20))
|
|
88
|
+
: variable.value
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div className={cn(
|
|
92
|
+
"flex items-center gap-2 p-2 border-b hover:bg-muted/30 transition-colors",
|
|
93
|
+
variable.isRequired && !variable.value && "bg-yellow-500/5"
|
|
94
|
+
)}>
|
|
95
|
+
{/* Secret indicator */}
|
|
96
|
+
<TooltipProvider>
|
|
97
|
+
<Tooltip>
|
|
98
|
+
<TooltipTrigger asChild>
|
|
99
|
+
<button
|
|
100
|
+
className={cn(
|
|
101
|
+
"p-1.5 rounded hover:bg-muted transition-colors",
|
|
102
|
+
readOnly && "pointer-events-none"
|
|
103
|
+
)}
|
|
104
|
+
onClick={() => !readOnly && onChange(index, "isSecret", !variable.isSecret)}
|
|
105
|
+
>
|
|
106
|
+
{variable.isSecret ? (
|
|
107
|
+
<Lock className="h-4 w-4 text-yellow-500" />
|
|
108
|
+
) : (
|
|
109
|
+
<Unlock className="h-4 w-4 text-muted-foreground" />
|
|
110
|
+
)}
|
|
111
|
+
</button>
|
|
112
|
+
</TooltipTrigger>
|
|
113
|
+
<TooltipContent>{variable.isSecret ? "Secret variable" : "Not a secret"}</TooltipContent>
|
|
114
|
+
</Tooltip>
|
|
115
|
+
</TooltipProvider>
|
|
116
|
+
|
|
117
|
+
{/* Key */}
|
|
118
|
+
<Input
|
|
119
|
+
value={variable.key}
|
|
120
|
+
onChange={(e) => onChange(index, "key", e.target.value)}
|
|
121
|
+
placeholder="KEY_NAME"
|
|
122
|
+
className="font-mono text-sm w-48 h-8"
|
|
123
|
+
readOnly={readOnly}
|
|
124
|
+
/>
|
|
125
|
+
|
|
126
|
+
<span className="text-muted-foreground">=</span>
|
|
127
|
+
|
|
128
|
+
{/* Value */}
|
|
129
|
+
<div className="flex-1 relative">
|
|
130
|
+
<Input
|
|
131
|
+
type={variable.isSecret && !showValue ? "password" : "text"}
|
|
132
|
+
value={variable.value}
|
|
133
|
+
onChange={(e) => onChange(index, "value", e.target.value)}
|
|
134
|
+
placeholder="value"
|
|
135
|
+
className="font-mono text-sm h-8 pr-16"
|
|
136
|
+
readOnly={readOnly}
|
|
137
|
+
/>
|
|
138
|
+
<div className="absolute right-1 top-1/2 -translate-y-1/2 flex items-center gap-1">
|
|
139
|
+
{variable.isSecret && (
|
|
140
|
+
<Button
|
|
141
|
+
variant="ghost"
|
|
142
|
+
size="sm"
|
|
143
|
+
className="h-6 w-6 p-0"
|
|
144
|
+
onClick={() => setShowValue(!showValue)}
|
|
145
|
+
>
|
|
146
|
+
{showValue ? (
|
|
147
|
+
<EyeOff className="h-3 w-3" />
|
|
148
|
+
) : (
|
|
149
|
+
<Eye className="h-3 w-3" />
|
|
150
|
+
)}
|
|
151
|
+
</Button>
|
|
152
|
+
)}
|
|
153
|
+
<Button
|
|
154
|
+
variant="ghost"
|
|
155
|
+
size="sm"
|
|
156
|
+
className="h-6 w-6 p-0"
|
|
157
|
+
onClick={copyValue}
|
|
158
|
+
>
|
|
159
|
+
{copied ? (
|
|
160
|
+
<CheckCircle2 className="h-3 w-3 text-green-500" />
|
|
161
|
+
) : (
|
|
162
|
+
<Copy className="h-3 w-3" />
|
|
163
|
+
)}
|
|
164
|
+
</Button>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
{/* Source */}
|
|
169
|
+
{showSource && variable.source && (
|
|
170
|
+
<Badge variant="outline" className="text-xs shrink-0">
|
|
171
|
+
{variable.source}
|
|
172
|
+
</Badge>
|
|
173
|
+
)}
|
|
174
|
+
|
|
175
|
+
{/* Required indicator */}
|
|
176
|
+
{variable.isRequired && !variable.value && (
|
|
177
|
+
<TooltipProvider>
|
|
178
|
+
<Tooltip>
|
|
179
|
+
<TooltipTrigger>
|
|
180
|
+
<AlertTriangle className="h-4 w-4 text-yellow-500" />
|
|
181
|
+
</TooltipTrigger>
|
|
182
|
+
<TooltipContent>Required variable is empty</TooltipContent>
|
|
183
|
+
</Tooltip>
|
|
184
|
+
</TooltipProvider>
|
|
185
|
+
)}
|
|
186
|
+
|
|
187
|
+
{/* Delete */}
|
|
188
|
+
{!readOnly && (
|
|
189
|
+
<Button
|
|
190
|
+
variant="ghost"
|
|
191
|
+
size="sm"
|
|
192
|
+
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
|
|
193
|
+
onClick={() => onDelete(index)}
|
|
194
|
+
>
|
|
195
|
+
<Trash2 className="h-4 w-4" />
|
|
196
|
+
</Button>
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function WakaEnvVarEditor({
|
|
203
|
+
variables: initialVariables,
|
|
204
|
+
onChange,
|
|
205
|
+
onSave,
|
|
206
|
+
onImport,
|
|
207
|
+
onExport,
|
|
208
|
+
readOnly = false,
|
|
209
|
+
showSource = false,
|
|
210
|
+
title = "Environment Variables",
|
|
211
|
+
className,
|
|
212
|
+
}: WakaEnvVarEditorProps) {
|
|
213
|
+
const [variables, setVariables] = React.useState<EnvVariable[]>(initialVariables)
|
|
214
|
+
const [searchQuery, setSearchQuery] = React.useState("")
|
|
215
|
+
const [hasChanges, setHasChanges] = React.useState(false)
|
|
216
|
+
const fileInputRef = React.useRef<HTMLInputElement>(null)
|
|
217
|
+
|
|
218
|
+
// Update internal state when props change
|
|
219
|
+
React.useEffect(() => {
|
|
220
|
+
setVariables(initialVariables)
|
|
221
|
+
setHasChanges(false)
|
|
222
|
+
}, [initialVariables])
|
|
223
|
+
|
|
224
|
+
const handleChange = (index: number, field: keyof EnvVariable, value: string | boolean) => {
|
|
225
|
+
const newVariables = [...variables]
|
|
226
|
+
newVariables[index] = { ...newVariables[index], [field]: value }
|
|
227
|
+
setVariables(newVariables)
|
|
228
|
+
setHasChanges(true)
|
|
229
|
+
onChange?.(newVariables)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const handleDelete = (index: number) => {
|
|
233
|
+
const newVariables = variables.filter((_, i) => i !== index)
|
|
234
|
+
setVariables(newVariables)
|
|
235
|
+
setHasChanges(true)
|
|
236
|
+
onChange?.(newVariables)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const handleAdd = () => {
|
|
240
|
+
const newVariables = [...variables, { key: "", value: "", isSecret: false }]
|
|
241
|
+
setVariables(newVariables)
|
|
242
|
+
setHasChanges(true)
|
|
243
|
+
onChange?.(newVariables)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const handleSave = () => {
|
|
247
|
+
onSave?.(variables)
|
|
248
|
+
setHasChanges(false)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const handleFileImport = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
252
|
+
const file = e.target.files?.[0]
|
|
253
|
+
if (file) {
|
|
254
|
+
const reader = new FileReader()
|
|
255
|
+
reader.onload = (event) => {
|
|
256
|
+
const content = event.target?.result as string
|
|
257
|
+
if (onImport) {
|
|
258
|
+
onImport(content)
|
|
259
|
+
} else {
|
|
260
|
+
// Default .env parsing
|
|
261
|
+
const parsed = content
|
|
262
|
+
.split("\n")
|
|
263
|
+
.filter((line) => line.trim() && !line.startsWith("#"))
|
|
264
|
+
.map((line) => {
|
|
265
|
+
const [key, ...valueParts] = line.split("=")
|
|
266
|
+
const value = valueParts.join("=").replace(/^["']|["']$/g, "")
|
|
267
|
+
return { key: key.trim(), value, isSecret: key.includes("SECRET") || key.includes("PASSWORD") || key.includes("KEY") }
|
|
268
|
+
})
|
|
269
|
+
setVariables(parsed)
|
|
270
|
+
setHasChanges(true)
|
|
271
|
+
onChange?.(parsed)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
reader.readAsText(file)
|
|
275
|
+
}
|
|
276
|
+
// Reset input
|
|
277
|
+
if (fileInputRef.current) {
|
|
278
|
+
fileInputRef.current.value = ""
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Filter variables
|
|
283
|
+
const filteredVariables = variables.filter((v) =>
|
|
284
|
+
v.key.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
285
|
+
(!v.isSecret && v.value.toLowerCase().includes(searchQuery.toLowerCase()))
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
// Count secrets
|
|
289
|
+
const secretCount = variables.filter((v) => v.isSecret).length
|
|
290
|
+
const requiredEmpty = variables.filter((v) => v.isRequired && !v.value).length
|
|
291
|
+
|
|
292
|
+
return (
|
|
293
|
+
<div className={cn("flex flex-col border rounded-lg bg-background", className)}>
|
|
294
|
+
{/* Header */}
|
|
295
|
+
<div className="flex items-center justify-between gap-4 p-3 border-b">
|
|
296
|
+
<div className="flex items-center gap-3">
|
|
297
|
+
<Variable className="h-5 w-5" />
|
|
298
|
+
<h3 className="font-semibold">{title}</h3>
|
|
299
|
+
<Badge variant="secondary">{variables.length}</Badge>
|
|
300
|
+
{secretCount > 0 && (
|
|
301
|
+
<Badge variant="outline" className="text-yellow-500">
|
|
302
|
+
<Lock className="h-3 w-3 mr-1" />
|
|
303
|
+
{secretCount} secrets
|
|
304
|
+
</Badge>
|
|
305
|
+
)}
|
|
306
|
+
{requiredEmpty > 0 && (
|
|
307
|
+
<Badge variant="outline" className="text-yellow-500">
|
|
308
|
+
<AlertTriangle className="h-3 w-3 mr-1" />
|
|
309
|
+
{requiredEmpty} missing
|
|
310
|
+
</Badge>
|
|
311
|
+
)}
|
|
312
|
+
</div>
|
|
313
|
+
|
|
314
|
+
<div className="flex items-center gap-2">
|
|
315
|
+
{onSave && hasChanges && (
|
|
316
|
+
<Button size="sm" onClick={handleSave}>
|
|
317
|
+
<Save className="h-4 w-4 mr-1" />
|
|
318
|
+
Save
|
|
319
|
+
</Button>
|
|
320
|
+
)}
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
{/* Toolbar */}
|
|
325
|
+
<div className="flex items-center gap-2 p-2 border-b bg-muted/30">
|
|
326
|
+
<div className="relative flex-1 max-w-sm">
|
|
327
|
+
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
328
|
+
<Input
|
|
329
|
+
placeholder="Search variables..."
|
|
330
|
+
value={searchQuery}
|
|
331
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
332
|
+
className="pl-8 h-8"
|
|
333
|
+
/>
|
|
334
|
+
</div>
|
|
335
|
+
|
|
336
|
+
{!readOnly && (
|
|
337
|
+
<>
|
|
338
|
+
<Button variant="outline" size="sm" className="h-8" onClick={handleAdd}>
|
|
339
|
+
<Plus className="h-4 w-4 mr-1" />
|
|
340
|
+
Add
|
|
341
|
+
</Button>
|
|
342
|
+
|
|
343
|
+
<input
|
|
344
|
+
type="file"
|
|
345
|
+
ref={fileInputRef}
|
|
346
|
+
accept=".env,.txt"
|
|
347
|
+
onChange={handleFileImport}
|
|
348
|
+
className="hidden"
|
|
349
|
+
/>
|
|
350
|
+
<Button
|
|
351
|
+
variant="outline"
|
|
352
|
+
size="sm"
|
|
353
|
+
className="h-8"
|
|
354
|
+
onClick={() => fileInputRef.current?.click()}
|
|
355
|
+
>
|
|
356
|
+
<Upload className="h-4 w-4 mr-1" />
|
|
357
|
+
Import
|
|
358
|
+
</Button>
|
|
359
|
+
</>
|
|
360
|
+
)}
|
|
361
|
+
|
|
362
|
+
{onExport && (
|
|
363
|
+
<Button variant="outline" size="sm" className="h-8" onClick={onExport}>
|
|
364
|
+
<Download className="h-4 w-4 mr-1" />
|
|
365
|
+
Export
|
|
366
|
+
</Button>
|
|
367
|
+
)}
|
|
368
|
+
</div>
|
|
369
|
+
|
|
370
|
+
{/* Variables list */}
|
|
371
|
+
<ScrollArea className="flex-1 max-h-[400px]">
|
|
372
|
+
{filteredVariables.length === 0 ? (
|
|
373
|
+
<div className="flex flex-col items-center justify-center h-32 text-muted-foreground">
|
|
374
|
+
<Variable className="h-8 w-8 mb-2" />
|
|
375
|
+
<span>No variables found</span>
|
|
376
|
+
{!readOnly && (
|
|
377
|
+
<Button variant="link" size="sm" onClick={handleAdd}>
|
|
378
|
+
Add your first variable
|
|
379
|
+
</Button>
|
|
380
|
+
)}
|
|
381
|
+
</div>
|
|
382
|
+
) : (
|
|
383
|
+
filteredVariables.map((variable, index) => (
|
|
384
|
+
<EnvVarRow
|
|
385
|
+
key={index}
|
|
386
|
+
variable={variable}
|
|
387
|
+
index={variables.indexOf(variable)}
|
|
388
|
+
onChange={handleChange}
|
|
389
|
+
onDelete={handleDelete}
|
|
390
|
+
readOnly={readOnly}
|
|
391
|
+
showSource={showSource}
|
|
392
|
+
/>
|
|
393
|
+
))
|
|
394
|
+
)}
|
|
395
|
+
</ScrollArea>
|
|
396
|
+
|
|
397
|
+
{/* Footer hint */}
|
|
398
|
+
{!readOnly && (
|
|
399
|
+
<div className="px-3 py-2 border-t bg-muted/30 text-xs text-muted-foreground">
|
|
400
|
+
💡 Click the lock icon to mark a variable as secret. Secrets are masked by default.
|
|
401
|
+
</div>
|
|
402
|
+
)}
|
|
403
|
+
</div>
|
|
404
|
+
)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Default sample variables for demo
|
|
408
|
+
export const defaultEnvVariables: EnvVariable[] = [
|
|
409
|
+
{ key: "NODE_ENV", value: "production", isRequired: true },
|
|
410
|
+
{ key: "DATABASE_URL", value: "postgresql://user:pass@localhost:5432/db", isSecret: true, isRequired: true },
|
|
411
|
+
{ key: "API_KEY", value: "sk-1234567890abcdef", isSecret: true },
|
|
412
|
+
{ key: "PORT", value: "3000" },
|
|
413
|
+
{ key: "LOG_LEVEL", value: "info" },
|
|
414
|
+
{ key: "REDIS_URL", value: "redis://localhost:6379", isSecret: true },
|
|
415
|
+
{ key: "AWS_ACCESS_KEY_ID", value: "", isSecret: true, isRequired: true },
|
|
416
|
+
{ key: "AWS_SECRET_ACCESS_KEY", value: "", isSecret: true, isRequired: true },
|
|
417
|
+
]
|