@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.
Files changed (77) hide show
  1. package/components/layout/index.ts +31 -0
  2. package/components/layout/layout-container.tsx +85 -0
  3. package/components/layout/root-layout-wrapper.tsx +33 -0
  4. package/components/layout/sidebar/data.tsx +88 -0
  5. package/components/layout/sidebar/main-sidebar.tsx +177 -0
  6. package/components/layout/sidebar/sidebar.tsx +750 -0
  7. package/components/layout/skeleton.tsx +15 -0
  8. package/components/ui/accordion.tsx +58 -0
  9. package/components/ui/alert-dialog.tsx +133 -0
  10. package/components/ui/alert.tsx +59 -0
  11. package/components/ui/aspect-ratio.tsx +7 -0
  12. package/components/ui/assets-header.tsx +50 -0
  13. package/components/ui/avatar.tsx +50 -0
  14. package/components/ui/badge.tsx +54 -0
  15. package/components/ui/breadcrumb.tsx +115 -0
  16. package/components/ui/button.tsx +83 -0
  17. package/components/ui/calendar.tsx +66 -0
  18. package/components/ui/card.tsx +79 -0
  19. package/components/ui/carousel.tsx +262 -0
  20. package/components/ui/certificate-editor.tsx +445 -0
  21. package/components/ui/chart.tsx +365 -0
  22. package/components/ui/checkbox.tsx +30 -0
  23. package/components/ui/collapsible.tsx +11 -0
  24. package/components/ui/command.tsx +153 -0
  25. package/components/ui/context-menu.tsx +200 -0
  26. package/components/ui/dialog.tsx +122 -0
  27. package/components/ui/drawer.tsx +118 -0
  28. package/components/ui/dropdown-menu.tsx +200 -0
  29. package/components/ui/environment-settings.tsx +173 -0
  30. package/components/ui/environment-variables-config.tsx +175 -0
  31. package/components/ui/file-import.tsx +177 -0
  32. package/components/ui/form.tsx +178 -0
  33. package/components/ui/hover-card.tsx +29 -0
  34. package/components/ui/index.ts +54 -0
  35. package/components/ui/input-otp.tsx +71 -0
  36. package/components/ui/input.tsx +23 -0
  37. package/components/ui/label.tsx +26 -0
  38. package/components/ui/logo.tsx +17 -0
  39. package/components/ui/menubar.tsx +236 -0
  40. package/components/ui/navigation-menu.tsx +128 -0
  41. package/components/ui/page-header.tsx +35 -0
  42. package/components/ui/pagination.tsx +112 -0
  43. package/components/ui/popover.tsx +31 -0
  44. package/components/ui/process-status.tsx +98 -0
  45. package/components/ui/progress.tsx +31 -0
  46. package/components/ui/radio-group.tsx +44 -0
  47. package/components/ui/resizable.tsx +45 -0
  48. package/components/ui/resource-settings.tsx +227 -0
  49. package/components/ui/scroll-area.tsx +48 -0
  50. package/components/ui/search-input.tsx +26 -0
  51. package/components/ui/secret-explorer.tsx +274 -0
  52. package/components/ui/secret-properties-editor.tsx +642 -0
  53. package/components/ui/select.tsx +162 -0
  54. package/components/ui/selected-asset.tsx +120 -0
  55. package/components/ui/separator.tsx +31 -0
  56. package/components/ui/sheet.tsx +140 -0
  57. package/components/ui/skeleton.tsx +15 -0
  58. package/components/ui/slider.tsx +28 -0
  59. package/components/ui/sonner.tsx +31 -0
  60. package/components/ui/switch.tsx +29 -0
  61. package/components/ui/table.tsx +117 -0
  62. package/components/ui/tabs.tsx +55 -0
  63. package/components/ui/textarea.tsx +22 -0
  64. package/components/ui/toast.tsx +131 -0
  65. package/components/ui/toaster.tsx +35 -0
  66. package/components/ui/toggle-group.tsx +61 -0
  67. package/components/ui/toggle.tsx +45 -0
  68. package/components/ui/tooltip.tsx +30 -0
  69. package/components/ui/user-menu.tsx +86 -0
  70. package/hooks/index.ts +5 -0
  71. package/hooks/use-auth.ts +10 -0
  72. package/hooks/use-mobile.ts +19 -0
  73. package/hooks/use-toast.ts +8 -0
  74. package/hooks/use-websocket.tsx +76 -0
  75. package/index.ts +11 -1
  76. package/lib/menu-utils.ts +48 -0
  77. 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
+ }