@orsetra/shared-ui 1.0.0 → 1.0.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/components/layout/index.ts +31 -0
- package/components/layout/layout-container.tsx +85 -0
- package/components/layout/root-layout-wrapper.tsx +33 -0
- package/components/layout/sidebar/data.tsx +88 -0
- package/components/layout/sidebar/main-sidebar.tsx +177 -0
- package/components/layout/sidebar/sidebar.tsx +750 -0
- package/components/layout/skeleton.tsx +15 -0
- package/components/ui/accordion.tsx +58 -0
- package/components/ui/alert-dialog.tsx +133 -0
- package/components/ui/alert.tsx +59 -0
- package/components/ui/aspect-ratio.tsx +7 -0
- package/components/ui/assets-header.tsx +50 -0
- package/components/ui/avatar.tsx +50 -0
- package/components/ui/badge.tsx +54 -0
- package/components/ui/breadcrumb.tsx +115 -0
- package/components/ui/button.tsx +83 -0
- package/components/ui/calendar.tsx +66 -0
- package/components/ui/card.tsx +79 -0
- package/components/ui/carousel.tsx +262 -0
- package/components/ui/certificate-editor.tsx +445 -0
- package/components/ui/chart.tsx +365 -0
- package/components/ui/checkbox.tsx +30 -0
- package/components/ui/collapsible.tsx +11 -0
- package/components/ui/command.tsx +153 -0
- package/components/ui/context-menu.tsx +200 -0
- package/components/ui/dialog.tsx +122 -0
- package/components/ui/drawer.tsx +118 -0
- package/components/ui/dropdown-menu.tsx +200 -0
- package/components/ui/environment-settings.tsx +173 -0
- package/components/ui/environment-variables-config.tsx +175 -0
- package/components/ui/file-import.tsx +177 -0
- package/components/ui/form.tsx +178 -0
- package/components/ui/hover-card.tsx +29 -0
- package/components/ui/index.ts +54 -0
- package/components/ui/input-otp.tsx +71 -0
- package/components/ui/input.tsx +23 -0
- package/components/ui/label.tsx +26 -0
- package/components/ui/logo.tsx +17 -0
- package/components/ui/menubar.tsx +236 -0
- package/components/ui/navigation-menu.tsx +128 -0
- package/components/ui/page-header.tsx +35 -0
- package/components/ui/pagination.tsx +112 -0
- package/components/ui/popover.tsx +31 -0
- package/components/ui/process-status.tsx +98 -0
- package/components/ui/progress.tsx +31 -0
- package/components/ui/radio-group.tsx +44 -0
- package/components/ui/resizable.tsx +45 -0
- package/components/ui/resource-settings.tsx +227 -0
- package/components/ui/scroll-area.tsx +48 -0
- package/components/ui/search-input.tsx +26 -0
- package/components/ui/secret-explorer.tsx +274 -0
- package/components/ui/secret-properties-editor.tsx +642 -0
- package/components/ui/select.tsx +162 -0
- package/components/ui/selected-asset.tsx +120 -0
- package/components/ui/separator.tsx +31 -0
- package/components/ui/sheet.tsx +140 -0
- package/components/ui/skeleton.tsx +15 -0
- package/components/ui/slider.tsx +28 -0
- package/components/ui/sonner.tsx +31 -0
- package/components/ui/switch.tsx +29 -0
- package/components/ui/table.tsx +117 -0
- package/components/ui/tabs.tsx +55 -0
- package/components/ui/textarea.tsx +22 -0
- package/components/ui/toast.tsx +131 -0
- package/components/ui/toaster.tsx +35 -0
- package/components/ui/toggle-group.tsx +61 -0
- package/components/ui/toggle.tsx +45 -0
- package/components/ui/tooltip.tsx +30 -0
- package/components/ui/user-menu.tsx +86 -0
- package/hooks/index.ts +5 -0
- package/hooks/use-auth.ts +10 -0
- package/hooks/use-mobile.ts +19 -0
- package/hooks/use-toast.ts +8 -0
- package/hooks/use-websocket.tsx +76 -0
- package/index.ts +11 -1
- package/lib/menu-utils.ts +48 -0
- package/package.json +36 -8
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react"
|
|
4
|
+
import { Button } from "./button"
|
|
5
|
+
import { Input } from "./input"
|
|
6
|
+
import { Label } from "./label"
|
|
7
|
+
import { Textarea } from "./textarea"
|
|
8
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs"
|
|
9
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select"
|
|
10
|
+
import { Key, Plus, Eye, EyeOff, Copy, Trash2, FileText, Code, RefreshCw, Info } from "lucide-react"
|
|
11
|
+
import { useToast } from "../../hooks/use-toast"
|
|
12
|
+
import { getSecretData, createSecret, updateSecret , getSecret} from "@/app/secrets/service"
|
|
13
|
+
import type { CreateSecretRequest, UpdateSecretRequest } from "@/app/secrets/models"
|
|
14
|
+
import { convertDataToText, convertTextToData, FileType } from "../../lib/secret-utils"
|
|
15
|
+
import { FileImport } from "./file-import"
|
|
16
|
+
|
|
17
|
+
interface SecretPropertiesEditorProps {
|
|
18
|
+
secretPath?: string
|
|
19
|
+
initialData?: Record<string, string>
|
|
20
|
+
description?: string
|
|
21
|
+
onDataChange?: (data: Record<string, string>) => void
|
|
22
|
+
onDescriptionChange?: (description: string) => void
|
|
23
|
+
onUpdate?: () => void
|
|
24
|
+
showUpdateButton?: boolean
|
|
25
|
+
className?: string
|
|
26
|
+
readOnly?: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function SecretPropertiesEditor({
|
|
30
|
+
secretPath,
|
|
31
|
+
initialData = {},
|
|
32
|
+
description = "",
|
|
33
|
+
onDataChange,
|
|
34
|
+
onDescriptionChange,
|
|
35
|
+
onUpdate,
|
|
36
|
+
showUpdateButton = true,
|
|
37
|
+
className = "",
|
|
38
|
+
readOnly = false
|
|
39
|
+
}: SecretPropertiesEditorProps) {
|
|
40
|
+
const { toast } = useToast()
|
|
41
|
+
const [fileType, setFileType] = useState<FileType>("application/json")
|
|
42
|
+
const [data, setData] = useState<Record<string, string>>(initialData)
|
|
43
|
+
const [textData, setTextData] = useState("")
|
|
44
|
+
const [versions, setVersions] = useState<any>({})
|
|
45
|
+
const [currentDescription, setCurrentDescription] = useState(description)
|
|
46
|
+
const [metadata, setMetadata] = useState<any>({})
|
|
47
|
+
const [loading, setLoading] = useState(false)
|
|
48
|
+
const [showSecretValues, setShowSecretValues] = useState<Record<string, boolean>>({})
|
|
49
|
+
const [newKeyValue, setNewKeyValue] = useState({ key: "", value: "" })
|
|
50
|
+
const [activeTab, setActiveTab] = useState<"view" | "text">("view")
|
|
51
|
+
const [secretExists, setSecretExists] = useState(false)
|
|
52
|
+
const [selectedVersion, setSelectedVersion] = useState<string>("")
|
|
53
|
+
const [hasChanges, setHasChanges] = useState(false)
|
|
54
|
+
const [originalData, setOriginalData] = useState<Record<string, string>>({})
|
|
55
|
+
const [originalDescription, setOriginalDescription] = useState("")
|
|
56
|
+
|
|
57
|
+
// Load secret data when secretPath changes
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (secretPath) {
|
|
60
|
+
loadSecretData()
|
|
61
|
+
loadVersions()
|
|
62
|
+
}
|
|
63
|
+
}, [secretPath])
|
|
64
|
+
|
|
65
|
+
// Convert data to text format when switching to text tab
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (activeTab === "text") {
|
|
68
|
+
try {
|
|
69
|
+
const jsonText = convertDataToText(data, fileType)
|
|
70
|
+
setTextData(jsonText)
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error("Error converting data to text:", error)
|
|
73
|
+
setTextData("{}")
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}, [activeTab, data])
|
|
77
|
+
|
|
78
|
+
// Sync with external data changes
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (initialData && Object.keys(initialData).length > 0) {
|
|
81
|
+
setData(initialData)
|
|
82
|
+
}
|
|
83
|
+
}, [initialData])
|
|
84
|
+
|
|
85
|
+
// Check for changes when data or description changes
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
checkForChanges()
|
|
88
|
+
}, [data, currentDescription, originalData, originalDescription])
|
|
89
|
+
|
|
90
|
+
const loadVersions = async () => {
|
|
91
|
+
if (!secretPath) return
|
|
92
|
+
const secret = await getSecret(secretPath)
|
|
93
|
+
setVersions(secret?.metadata.versions || {})
|
|
94
|
+
// Set selected version if not already set and we have versions
|
|
95
|
+
if (!selectedVersion && secret?.metadata?.versions) {
|
|
96
|
+
const versions = Object.keys(secret.metadata.versions)
|
|
97
|
+
if (versions.length > 0) {
|
|
98
|
+
const latestVersion = Math.max(...versions.map(Number)).toString()
|
|
99
|
+
setSelectedVersion(latestVersion)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const loadSecretData = async (version?: string) => {
|
|
105
|
+
if (!secretPath) return
|
|
106
|
+
|
|
107
|
+
setLoading(true)
|
|
108
|
+
try {
|
|
109
|
+
// For Vault KV v2, we can specify version in the path or as a parameter
|
|
110
|
+
const versionedPath = version ? `${secretPath}?version=${version}` : secretPath
|
|
111
|
+
const secretData = await getSecretData(versionedPath)
|
|
112
|
+
if (secretData) {
|
|
113
|
+
const dataObj = secretData.data || {}
|
|
114
|
+
const descriptionStr = secretData.metadata?.description || ""
|
|
115
|
+
|
|
116
|
+
setData(dataObj)
|
|
117
|
+
setCurrentDescription(descriptionStr)
|
|
118
|
+
setMetadata(secretData.metadata || {})
|
|
119
|
+
setSecretExists(true)
|
|
120
|
+
|
|
121
|
+
// Store original values for change detection
|
|
122
|
+
setOriginalData(dataObj)
|
|
123
|
+
setOriginalDescription(descriptionStr)
|
|
124
|
+
setHasChanges(false)
|
|
125
|
+
} else {
|
|
126
|
+
setData({})
|
|
127
|
+
setCurrentDescription("")
|
|
128
|
+
setMetadata({})
|
|
129
|
+
setSecretExists(false)
|
|
130
|
+
setOriginalData({})
|
|
131
|
+
setOriginalDescription("")
|
|
132
|
+
setHasChanges(false)
|
|
133
|
+
}
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.error("Error loading secret:", error)
|
|
136
|
+
toast({
|
|
137
|
+
variant: "destructive",
|
|
138
|
+
title: "Error loading secret",
|
|
139
|
+
description: "Failed to load secret data."
|
|
140
|
+
})
|
|
141
|
+
} finally {
|
|
142
|
+
setLoading(false)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const checkForChanges = () => {
|
|
147
|
+
const dataChanged = JSON.stringify(data) !== JSON.stringify(originalData)
|
|
148
|
+
const descriptionChanged = currentDescription !== originalDescription
|
|
149
|
+
setHasChanges(dataChanged || descriptionChanged)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const refreshSecret = async () => {
|
|
153
|
+
if (secretPath) {
|
|
154
|
+
await loadSecretData(selectedVersion)
|
|
155
|
+
await loadVersions()
|
|
156
|
+
toast({
|
|
157
|
+
variant: "success",
|
|
158
|
+
title: "Secret refreshed",
|
|
159
|
+
description: "Secret data has been reloaded."
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const handleVersionChange = async (version: string) => {
|
|
165
|
+
setSelectedVersion(version)
|
|
166
|
+
await loadSecretData(version)
|
|
167
|
+
toast({
|
|
168
|
+
variant: "success",
|
|
169
|
+
title: "Version loaded",
|
|
170
|
+
description: `Loaded version ${version} of the secret.`
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const getAvailableVersions = () => {
|
|
175
|
+
if (!versions) return []
|
|
176
|
+
return Object.entries(versions)
|
|
177
|
+
.filter(([_, versionData]: [string, any]) => !versionData.destroyed && !versionData.deletion_time)
|
|
178
|
+
.map(([versionNum, versionData]: [string, any]) => ({
|
|
179
|
+
value: versionNum,
|
|
180
|
+
label: `${secretPath}:${versionNum}`,
|
|
181
|
+
createdTime: versionData.created_time
|
|
182
|
+
}))
|
|
183
|
+
.sort((a, b) => parseInt(b.value) - parseInt(a.value)) // Sort by version number descending
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const handleTabChange = (value: string) => {
|
|
187
|
+
if (value === "text" && activeTab === "view") {
|
|
188
|
+
try {
|
|
189
|
+
const jsonText = convertDataToText(data, fileType)
|
|
190
|
+
setTextData(jsonText)
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.error("Error converting data to text:", error)
|
|
193
|
+
setTextData("{}")
|
|
194
|
+
}
|
|
195
|
+
} else if (value === "view" && activeTab === "text") {
|
|
196
|
+
try {
|
|
197
|
+
const stringData = convertTextToData(textData, fileType)
|
|
198
|
+
setData(stringData)
|
|
199
|
+
onDataChange?.(stringData)
|
|
200
|
+
} catch (error: any) {
|
|
201
|
+
toast({
|
|
202
|
+
variant: "destructive",
|
|
203
|
+
title: "Invalid JSON",
|
|
204
|
+
description: error.message || "Please enter valid JSON format."
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
setActiveTab(value as "view" | "text")
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const toggleSecretVisibility = (key: string) => {
|
|
212
|
+
setShowSecretValues(prev => ({
|
|
213
|
+
...prev,
|
|
214
|
+
[key]: !prev[key]
|
|
215
|
+
}))
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const copyToClipboard = (value: string) => {
|
|
219
|
+
navigator.clipboard.writeText(value)
|
|
220
|
+
toast({
|
|
221
|
+
variant: "success",
|
|
222
|
+
title: "Copied to clipboard",
|
|
223
|
+
description: "Secret value has been copied."
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const addKeyValue = () => {
|
|
228
|
+
if (!newKeyValue.key || !newKeyValue.value) return
|
|
229
|
+
|
|
230
|
+
const newData = {
|
|
231
|
+
...data,
|
|
232
|
+
[newKeyValue.key]: newKeyValue.value
|
|
233
|
+
}
|
|
234
|
+
setData(newData)
|
|
235
|
+
onDataChange?.(newData)
|
|
236
|
+
setNewKeyValue({ key: "", value: "" })
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const removeKeyValue = (key: string) => {
|
|
240
|
+
const newData = { ...data }
|
|
241
|
+
delete newData[key]
|
|
242
|
+
setData(newData)
|
|
243
|
+
onDataChange?.(newData)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const updateKeyValue = (oldKey: string, newKey: string, value: string) => {
|
|
247
|
+
const newData = { ...data }
|
|
248
|
+
if (oldKey !== newKey) {
|
|
249
|
+
delete newData[oldKey]
|
|
250
|
+
}
|
|
251
|
+
newData[newKey] = value
|
|
252
|
+
setData(newData)
|
|
253
|
+
onDataChange?.(newData)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const handleUpdate = async () => {
|
|
257
|
+
if (!secretPath) {
|
|
258
|
+
onUpdate?.()
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
const request: CreateSecretRequest | UpdateSecretRequest = {
|
|
264
|
+
data: data,
|
|
265
|
+
metadata: {
|
|
266
|
+
description: currentDescription || undefined
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (secretExists) {
|
|
271
|
+
await updateSecret(secretPath, request as UpdateSecretRequest)
|
|
272
|
+
toast({
|
|
273
|
+
variant: "success",
|
|
274
|
+
title: "Secret updated",
|
|
275
|
+
description: `Secret "${secretPath}" has been updated successfully.`
|
|
276
|
+
})
|
|
277
|
+
} else {
|
|
278
|
+
await createSecret(secretPath, request as CreateSecretRequest)
|
|
279
|
+
setSecretExists(true)
|
|
280
|
+
toast({
|
|
281
|
+
variant: "success",
|
|
282
|
+
title: "Secret created",
|
|
283
|
+
description: `Secret "${secretPath}" has been created successfully.`
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Reset change tracking after successful update
|
|
288
|
+
setOriginalData(data)
|
|
289
|
+
setOriginalDescription(currentDescription)
|
|
290
|
+
setHasChanges(false)
|
|
291
|
+
|
|
292
|
+
onUpdate?.()
|
|
293
|
+
} catch (error) {
|
|
294
|
+
console.error("Error updating secret:", error)
|
|
295
|
+
toast({
|
|
296
|
+
variant: "destructive",
|
|
297
|
+
title: "Error updating secret",
|
|
298
|
+
description: "Failed to save secret data."
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (loading) {
|
|
304
|
+
return (
|
|
305
|
+
<div className={`space-y-6 ${className}`}>
|
|
306
|
+
<div className="animate-pulse">
|
|
307
|
+
<div className="h-4 bg-ibm-gray-20 mb-4"></div>
|
|
308
|
+
<div className="h-32 bg-ibm-gray-20"></div>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return (
|
|
315
|
+
<div className={`space-y-6 ${className}`}>
|
|
316
|
+
{/* Properties Section */}
|
|
317
|
+
<div>
|
|
318
|
+
<div className="flex items-center justify-between mb-4">
|
|
319
|
+
<Label className="text-sm font-medium text-ibm-gray-100">Properties</Label>
|
|
320
|
+
{secretPath && (
|
|
321
|
+
<div className="flex items-center gap-3">
|
|
322
|
+
<Button
|
|
323
|
+
variant="ghost"
|
|
324
|
+
size="sm"
|
|
325
|
+
onClick={refreshSecret}
|
|
326
|
+
style={{borderRadius: 0}}
|
|
327
|
+
leftIcon={<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />}
|
|
328
|
+
disabled={loading}
|
|
329
|
+
>
|
|
330
|
+
Refresh
|
|
331
|
+
</Button>
|
|
332
|
+
|
|
333
|
+
{/* Version Selector */}
|
|
334
|
+
{getAvailableVersions().length > 0 && (
|
|
335
|
+
<Select value={selectedVersion} onValueChange={handleVersionChange}>
|
|
336
|
+
<SelectTrigger className="w-64">
|
|
337
|
+
<SelectValue placeholder="Select a version" />
|
|
338
|
+
</SelectTrigger>
|
|
339
|
+
<SelectContent>
|
|
340
|
+
{getAvailableVersions().map((version) => (
|
|
341
|
+
<SelectItem key={version.value} value={version.value}>
|
|
342
|
+
<div className="flex flex-col">
|
|
343
|
+
<span className="font-medium">{version.label}</span>
|
|
344
|
+
<span className="text-xs text-ibm-gray-70">
|
|
345
|
+
{new Date(version.createdTime).toLocaleString()}
|
|
346
|
+
</span>
|
|
347
|
+
</div>
|
|
348
|
+
</SelectItem>
|
|
349
|
+
))}
|
|
350
|
+
</SelectContent>
|
|
351
|
+
</Select>
|
|
352
|
+
)}
|
|
353
|
+
</div>
|
|
354
|
+
)}
|
|
355
|
+
</div>
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
<Tabs value={activeTab} onValueChange={handleTabChange} className="w-full">
|
|
359
|
+
<div className="border-b border-ibm-gray-20">
|
|
360
|
+
<TabsList className="h-auto bg-transparent border-0 p-0 justify-start gap-0">
|
|
361
|
+
<TabsTrigger
|
|
362
|
+
value="view"
|
|
363
|
+
className="flex items-center gap-2 px-4 py-2 text-sm font-medium transition-colors border-b-2 border-transparent data-[state=active]:border-ibm-blue-60 data-[state=active]:text-ibm-blue-60 text-ibm-gray-70 hover:text-ibm-blue-60 bg-transparent rounded-none"
|
|
364
|
+
>
|
|
365
|
+
<FileText className="w-4 h-4" />
|
|
366
|
+
View Mode
|
|
367
|
+
</TabsTrigger>
|
|
368
|
+
<TabsTrigger
|
|
369
|
+
value="text"
|
|
370
|
+
className="flex items-center gap-2 px-4 py-2 text-sm font-medium transition-colors border-b-2 border-transparent data-[state=active]:border-ibm-blue-60 data-[state=active]:text-ibm-blue-60 text-ibm-gray-70 hover:text-ibm-blue-60 bg-transparent rounded-none"
|
|
371
|
+
>
|
|
372
|
+
<Code className="w-4 h-4" />
|
|
373
|
+
Text Mode
|
|
374
|
+
</TabsTrigger>
|
|
375
|
+
<TabsTrigger
|
|
376
|
+
value="metadata"
|
|
377
|
+
className="flex items-center gap-2 px-4 py-2 text-sm font-medium transition-colors border-b-2 border-transparent data-[state=active]:border-ibm-blue-60 data-[state=active]:text-ibm-blue-60 text-ibm-gray-70 hover:text-ibm-blue-60 bg-transparent rounded-none"
|
|
378
|
+
>
|
|
379
|
+
<Info className="w-4 h-4" />
|
|
380
|
+
Metadata
|
|
381
|
+
</TabsTrigger>
|
|
382
|
+
</TabsList>
|
|
383
|
+
</div>
|
|
384
|
+
|
|
385
|
+
<TabsContent value="view" className="mt-4">
|
|
386
|
+
<div className="space-y-4">
|
|
387
|
+
<div>
|
|
388
|
+
|
|
389
|
+
<Label className="text-sm font-medium text-ibm-gray-100 ml-3 mt-3">Table View</Label>
|
|
390
|
+
<div className="space-y-1 max-h-96 overflow-y-auto border-none">
|
|
391
|
+
|
|
392
|
+
{/* Existing Properties */}
|
|
393
|
+
{Object.entries(data || {}).map(([key, value]) => (
|
|
394
|
+
<div key={key} className="grid grid-cols-6 gap-2 px-3 py-2 hover:bg-ibm-gray-10 transition-colors">
|
|
395
|
+
{/* Key - with actions */}
|
|
396
|
+
<div className="col-span-2">
|
|
397
|
+
<div className="relative">
|
|
398
|
+
<Input
|
|
399
|
+
value={key}
|
|
400
|
+
readOnly
|
|
401
|
+
className="text-sm pr-16 border-ibm-gray-20 h-8 bg-ibm-gray-10 font-medium"
|
|
402
|
+
/>
|
|
403
|
+
<div className="absolute right-1 top-1/2 transform -translate-y-1/2 flex items-center gap-1">
|
|
404
|
+
<Button
|
|
405
|
+
variant="ghost"
|
|
406
|
+
size="xs"
|
|
407
|
+
onClick={() => copyToClipboard(key)}
|
|
408
|
+
className="h-6 w-6 p-0"
|
|
409
|
+
>
|
|
410
|
+
<Copy className="w-3 h-3" />
|
|
411
|
+
</Button>
|
|
412
|
+
{!readOnly && (
|
|
413
|
+
<Button
|
|
414
|
+
variant="ghost"
|
|
415
|
+
size="xs"
|
|
416
|
+
onClick={() => removeKeyValue(key)}
|
|
417
|
+
className="text-ibm-red-60 hover:text-ibm-red-70 hover:bg-ibm-red-10 h-6 w-6 p-0"
|
|
418
|
+
>
|
|
419
|
+
<Trash2 className="w-3 h-3" />
|
|
420
|
+
</Button>
|
|
421
|
+
)}
|
|
422
|
+
</div>
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
|
|
426
|
+
{/* Value */}
|
|
427
|
+
<div className="col-span-4">
|
|
428
|
+
<div className="relative">
|
|
429
|
+
<Input
|
|
430
|
+
type={showSecretValues[key] ? "text" : "password"}
|
|
431
|
+
value={value}
|
|
432
|
+
onChange={(e) => updateKeyValue(key, key, e.target.value)}
|
|
433
|
+
placeholder="Enter value..."
|
|
434
|
+
className="text-sm pr-16 border-ibm-gray-20 h-8"
|
|
435
|
+
readOnly={readOnly}
|
|
436
|
+
/>
|
|
437
|
+
<div className="absolute right-1 top-1/2 transform -translate-y-1/2 flex items-center gap-1">
|
|
438
|
+
<Button
|
|
439
|
+
variant="ghost"
|
|
440
|
+
size="xs"
|
|
441
|
+
onClick={() => toggleSecretVisibility(key)}
|
|
442
|
+
leftIcon={showSecretValues[key] ? <EyeOff className="w-3 h-3" /> : <Eye className="w-3 h-3" />}
|
|
443
|
+
className="h-6 w-6 p-0"
|
|
444
|
+
>
|
|
445
|
+
</Button>
|
|
446
|
+
<Button
|
|
447
|
+
variant="ghost"
|
|
448
|
+
size="xs"
|
|
449
|
+
leftIcon={<Copy className="w-3 h-3" />}
|
|
450
|
+
onClick={() => copyToClipboard(value)}
|
|
451
|
+
className="h-6 w-6 p-0"
|
|
452
|
+
>
|
|
453
|
+
</Button>
|
|
454
|
+
</div>
|
|
455
|
+
</div>
|
|
456
|
+
</div>
|
|
457
|
+
</div>
|
|
458
|
+
))}
|
|
459
|
+
|
|
460
|
+
{/* Add new property row */}
|
|
461
|
+
{!readOnly && (
|
|
462
|
+
<div className="grid grid-cols-6 gap-2 px-3 py-2 hover:bg-ibm-gray-10 transition-colors border-t border-dashed border-ibm-gray-20 bg-white sticky bottom-0">
|
|
463
|
+
{/* New Key */}
|
|
464
|
+
<div className="col-span-2">
|
|
465
|
+
<Input
|
|
466
|
+
value={newKeyValue.key}
|
|
467
|
+
onChange={(e) => setNewKeyValue(prev => ({ ...prev, key: e.target.value }))}
|
|
468
|
+
onKeyDown={(e) => {
|
|
469
|
+
if (e.key === 'Enter' && newKeyValue.key && newKeyValue.value) {
|
|
470
|
+
e.preventDefault()
|
|
471
|
+
addKeyValue()
|
|
472
|
+
}
|
|
473
|
+
}}
|
|
474
|
+
placeholder="Property key..."
|
|
475
|
+
className="text-sm border-ibm-gray-20 h-8"
|
|
476
|
+
/>
|
|
477
|
+
</div>
|
|
478
|
+
|
|
479
|
+
{/* New Value with Add Button */}
|
|
480
|
+
<div className="col-span-4">
|
|
481
|
+
<div className="relative">
|
|
482
|
+
<Input
|
|
483
|
+
type="password"
|
|
484
|
+
value={newKeyValue.value}
|
|
485
|
+
onChange={(e) => setNewKeyValue(prev => ({ ...prev, value: e.target.value }))}
|
|
486
|
+
onKeyDown={(e) => {
|
|
487
|
+
if (e.key === 'Enter' && newKeyValue.key && newKeyValue.value) {
|
|
488
|
+
e.preventDefault()
|
|
489
|
+
addKeyValue()
|
|
490
|
+
}
|
|
491
|
+
}}
|
|
492
|
+
placeholder="Property value..."
|
|
493
|
+
className="text-sm border-ibm-gray-20 h-8 pr-10"
|
|
494
|
+
/>
|
|
495
|
+
<div className="absolute right-1 top-1/2 transform -translate-y-1/2">
|
|
496
|
+
<Button
|
|
497
|
+
variant="ghost"
|
|
498
|
+
size="xs"
|
|
499
|
+
onClick={addKeyValue}
|
|
500
|
+
disabled={!newKeyValue.key || !newKeyValue.value}
|
|
501
|
+
className="text-ibm-green-60 hover:text-ibm-green-70 hover:bg-ibm-green-10 disabled:text-ibm-gray-40 h-6 w-6 p-0"
|
|
502
|
+
>
|
|
503
|
+
<Plus className="w-3 h-3" />
|
|
504
|
+
</Button>
|
|
505
|
+
</div>
|
|
506
|
+
</div>
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
509
|
+
)}
|
|
510
|
+
|
|
511
|
+
{/* Empty state message */}
|
|
512
|
+
{Object.keys(data || {}).length === 0 && (
|
|
513
|
+
<div className="text-center py-6">
|
|
514
|
+
<Key className="w-8 h-8 text-ibm-gray-30 mx-auto mb-2" />
|
|
515
|
+
<p className="text-sm text-ibm-gray-70">
|
|
516
|
+
{readOnly ? "No properties found" : "Add your first property using the form below"}
|
|
517
|
+
</p>
|
|
518
|
+
</div>
|
|
519
|
+
)}
|
|
520
|
+
</div>
|
|
521
|
+
</div>
|
|
522
|
+
</div>
|
|
523
|
+
</TabsContent>
|
|
524
|
+
|
|
525
|
+
<TabsContent value="text" className="mt-4">
|
|
526
|
+
<div className="space-y-4">
|
|
527
|
+
<div>
|
|
528
|
+
<div className="flex items-center gap-2">
|
|
529
|
+
<Label className="text-sm font-medium text-ibm-gray-100">Format:</Label>
|
|
530
|
+
<Select value={fileType} onValueChange={(value) => setFileType(value as FileType)}>
|
|
531
|
+
<SelectTrigger className="w-30 h-6" >
|
|
532
|
+
<SelectValue placeholder="Select a certificate type" />
|
|
533
|
+
</SelectTrigger>
|
|
534
|
+
<SelectContent>
|
|
535
|
+
<SelectItem value="application/json">JSON</SelectItem>
|
|
536
|
+
<SelectItem value="text/properties">Properties</SelectItem>
|
|
537
|
+
<SelectItem value="application/yaml">YAML</SelectItem>
|
|
538
|
+
</SelectContent>
|
|
539
|
+
</Select>
|
|
540
|
+
|
|
541
|
+
</div>
|
|
542
|
+
<Textarea
|
|
543
|
+
value={textData}
|
|
544
|
+
onChange={(e) => setTextData(e.target.value)}
|
|
545
|
+
placeholder='{\n "key1": "value1",\n "key2": "value2"\n}'
|
|
546
|
+
className="mt-2 min-h-[200px] max-h-80 font-mono text-sm resize-none"
|
|
547
|
+
readOnly={readOnly}
|
|
548
|
+
/>
|
|
549
|
+
<div className="mt-2 flex items-center gap-2">
|
|
550
|
+
<FileImport
|
|
551
|
+
onFileContent={(content, fileName, fileType) => {
|
|
552
|
+
console.log("fileType", fileType, "content", content)
|
|
553
|
+
setTextData(content)
|
|
554
|
+
setFileType(fileType as FileType)
|
|
555
|
+
}}
|
|
556
|
+
acceptedTypes={["application/json", "application/yaml", "text/properties"]}
|
|
557
|
+
buttonText="Import content"
|
|
558
|
+
buttonVariant="ghost"
|
|
559
|
+
buttonSize="sm"
|
|
560
|
+
/>
|
|
561
|
+
<p className="text-xs text-ibm-gray-70">
|
|
562
|
+
Enter your secret data in JSON, YAML or Properties format. All values will be converted to key/value strings.
|
|
563
|
+
</p>
|
|
564
|
+
</div>
|
|
565
|
+
</div>
|
|
566
|
+
</div>
|
|
567
|
+
</TabsContent>
|
|
568
|
+
|
|
569
|
+
<TabsContent value="metadata" className="mt-4">
|
|
570
|
+
<div className="space-y-4">
|
|
571
|
+
<div>
|
|
572
|
+
<Label className="text-sm font-medium text-ibm-gray-100">Secret Metadata</Label>
|
|
573
|
+
<div className="mt-2 p-4 bg-ibm-gray-10 border border-ibm-gray-20 rounded">
|
|
574
|
+
{Object.keys(metadata).length > 0 ? (
|
|
575
|
+
<div className="space-y-3">
|
|
576
|
+
{Object.entries(metadata).map(([key, value]) => (
|
|
577
|
+
<div key={key} className="flex items-start gap-3">
|
|
578
|
+
<span className="text-sm font-medium text-ibm-gray-100 min-w-[120px] capitalize">
|
|
579
|
+
{key.replace(/_/g, ' ')}:
|
|
580
|
+
</span>
|
|
581
|
+
<span className="text-sm text-ibm-gray-70 break-all">
|
|
582
|
+
{typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value || 'N/A')}
|
|
583
|
+
</span>
|
|
584
|
+
</div>
|
|
585
|
+
))}
|
|
586
|
+
</div>
|
|
587
|
+
) : (
|
|
588
|
+
<div className="text-center py-6">
|
|
589
|
+
<Info className="w-8 h-8 text-ibm-gray-30 mx-auto mb-2" />
|
|
590
|
+
<p className="text-sm text-ibm-gray-70">
|
|
591
|
+
No metadata available for this secret
|
|
592
|
+
</p>
|
|
593
|
+
</div>
|
|
594
|
+
)}
|
|
595
|
+
</div>
|
|
596
|
+
<p className="text-xs text-ibm-gray-70 mt-1">
|
|
597
|
+
This information is automatically generated by the secret management system.
|
|
598
|
+
</p>
|
|
599
|
+
</div>
|
|
600
|
+
|
|
601
|
+
{/* Description Field */}
|
|
602
|
+
<div>
|
|
603
|
+
{readOnly ? (
|
|
604
|
+
<div className="p-3 bg-ibm-gray-10 border border-ibm-gray-20 rounded">
|
|
605
|
+
<p className="text-sm text-ibm-gray-70">
|
|
606
|
+
{currentDescription || "No description provided"}
|
|
607
|
+
</p>
|
|
608
|
+
</div>
|
|
609
|
+
) : (
|
|
610
|
+
<Input
|
|
611
|
+
value={currentDescription}
|
|
612
|
+
onChange={(e) => {
|
|
613
|
+
setCurrentDescription(e.target.value)
|
|
614
|
+
onDescriptionChange?.(e.target.value)
|
|
615
|
+
}}
|
|
616
|
+
placeholder="Enter secret description..."
|
|
617
|
+
className="text-sm border-ibm-gray-20"
|
|
618
|
+
/>
|
|
619
|
+
)}
|
|
620
|
+
</div>
|
|
621
|
+
</div>
|
|
622
|
+
</TabsContent>
|
|
623
|
+
</Tabs>
|
|
624
|
+
</div>
|
|
625
|
+
|
|
626
|
+
{/* Update Button */}
|
|
627
|
+
{showUpdateButton && !readOnly && (
|
|
628
|
+
<div className="flex justify-end pt-4 border-b-0">
|
|
629
|
+
<Button
|
|
630
|
+
variant="tertiary"
|
|
631
|
+
style={{borderRadius: 0}}
|
|
632
|
+
size="sm"
|
|
633
|
+
onClick={handleUpdate}
|
|
634
|
+
disabled={!hasChanges || Object.keys(data || {}).length === 0}
|
|
635
|
+
>
|
|
636
|
+
Commit changes
|
|
637
|
+
</Button>
|
|
638
|
+
</div>
|
|
639
|
+
)}
|
|
640
|
+
</div>
|
|
641
|
+
)
|
|
642
|
+
}
|