@orsetra/shared-ui 1.0.0 → 1.0.1

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 (76) 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 +10 -1
  76. package/package.json +35 -8
@@ -0,0 +1,445 @@
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, Eye, EyeOff, Copy, FileText, RefreshCw, Info } from "lucide-react"
11
+ import { useToast } from "../../hooks/use-toast"
12
+ import { getCertificateData, createCertificate, updateCertificate, getCertificate } from "@/app/secrets/service"
13
+ import type { CreateSecretRequest, UpdateSecretRequest } from "@/app/secrets/models"
14
+ import { FileImport } from "./file-import"
15
+
16
+ interface CertificateEditorProps {
17
+ secretPath?: string
18
+ initialContent?: string
19
+ description?: string
20
+ onContentChange?: (content: string) => void
21
+ onDescriptionChange?: (description: string) => void
22
+ onUpdate?: () => void
23
+ showUpdateButton?: boolean
24
+ className?: string
25
+ readOnly?: boolean
26
+ }
27
+
28
+ export function CertificateEditor({
29
+ secretPath,
30
+ initialContent = "",
31
+ description = "",
32
+ onContentChange,
33
+ onDescriptionChange,
34
+ onUpdate,
35
+ showUpdateButton = true,
36
+ className = "",
37
+ readOnly = false
38
+ }: CertificateEditorProps) {
39
+ const { toast } = useToast()
40
+ const [versions, setVersions] = useState<any>({})
41
+ const [metadata, setMetadata] = useState<any>({})
42
+ const [loading, setLoading] = useState(false)
43
+ const [showContent, setShowContent] = useState(false)
44
+ const [activeTab, setActiveTab] = useState<"content" | "metadata">("content")
45
+ const [secretExists, setSecretExists] = useState(false)
46
+ const [expirationDate, setExpirationDate] = useState<Date | null>(null)
47
+ const [selectedVersion, setSelectedVersion] = useState<string>("")
48
+ const [hasChanges, setHasChanges] = useState(false)
49
+ const [originalContent, setOriginalContent] = useState<string>("")
50
+ const [originalDescription, setOriginalDescription] = useState("")
51
+ const [createForm, setCreateForm] = useState<{description: string, content: string, fileType: string}>({
52
+ description: description,
53
+ content: initialContent,
54
+ fileType: ""
55
+ })
56
+ // Load secret data when secretPath changes
57
+ useEffect(() => {
58
+ if (secretPath) {
59
+ loadSecretData()
60
+ loadVersions()
61
+ }
62
+ }, [secretPath])
63
+
64
+ // Sync with external content changes
65
+ useEffect(() => {
66
+ if (initialContent) {
67
+ setCreateForm(prev => ({ ...prev, content: initialContent }))
68
+ }
69
+ }, [initialContent])
70
+
71
+ // Check for changes when content or description changes
72
+ useEffect(() => {
73
+ checkForChanges()
74
+ }, [createForm.content, createForm.description, originalContent, originalDescription])
75
+
76
+ const loadVersions = async () => {
77
+ if (!secretPath) return
78
+ const secret = await getCertificate(secretPath)
79
+ setVersions(secret?.metadata.versions || {})
80
+ // Set selected version if not already set and we have versions
81
+ if (!selectedVersion && secret?.metadata?.versions) {
82
+ const versions = Object.keys(secret.metadata.versions)
83
+ if (versions.length > 0) {
84
+ const latestVersion = Math.max(...versions.map(Number)).toString()
85
+ setSelectedVersion(latestVersion)
86
+ }
87
+ }
88
+ }
89
+
90
+ const loadSecretData = async (version?: string) => {
91
+ if (!secretPath) return
92
+
93
+ setLoading(true)
94
+ try {
95
+ // For Vault KV v2, we can specify version in the path or as a parameter
96
+ const versionedPath = version ? `${secretPath}?version=${version}` : secretPath
97
+ const secretData = await getCertificateData(versionedPath)
98
+ if (secretData) {
99
+ const contentStr = secretData.data?.content || ""
100
+ const descriptionStr = secretData.metadata?.description || ""
101
+ const certificateTypeStr = secretData.metadata?.certificate_type || "pem"
102
+ setCreateForm(prev => ({ ...prev, content: contentStr, description: descriptionStr, fileType: certificateTypeStr }))
103
+ setMetadata(secretData.metadata || {})
104
+ setSecretExists(true)
105
+
106
+ // Store original values for change detection
107
+ setOriginalContent(contentStr)
108
+ setOriginalDescription(descriptionStr)
109
+ setHasChanges(false)
110
+ } else {
111
+ setCreateForm(prev => ({ ...prev, content: "", description: "", fileType: "" }))
112
+ setMetadata({})
113
+ setSecretExists(false)
114
+ setOriginalContent("")
115
+ setOriginalDescription("")
116
+ setHasChanges(false)
117
+ }
118
+ } catch (error) {
119
+ console.error("Error loading certificate:", error)
120
+ toast({
121
+ variant: "destructive",
122
+ title: "Error loading certificate",
123
+ description: "Failed to load certificate data."
124
+ })
125
+ } finally {
126
+ setLoading(false)
127
+ }
128
+ }
129
+
130
+ const checkForChanges = () => {
131
+ const contentChanged = createForm.content !== originalContent
132
+ const descriptionChanged = createForm.description !== originalDescription
133
+ setHasChanges(contentChanged || descriptionChanged)
134
+ }
135
+
136
+ const refreshCertificate = async () => {
137
+ if (secretPath) {
138
+ await loadSecretData(selectedVersion)
139
+ await loadVersions()
140
+ toast({
141
+ variant: "success",
142
+ title: "Certificate refreshed",
143
+ description: "Certificate data has been reloaded."
144
+ })
145
+ }
146
+ }
147
+
148
+ const handleVersionChange = async (version: string) => {
149
+ setSelectedVersion(version)
150
+ await loadSecretData(version)
151
+ toast({
152
+ variant: "success",
153
+ title: "Version loaded",
154
+ description: `Loaded version ${version} of the certificate.`
155
+ })
156
+ }
157
+
158
+ const getAvailableVersions = () => {
159
+ if (!versions) return []
160
+ return Object.entries(versions)
161
+ .filter(([_, versionData]: [string, any]) => !versionData.destroyed && !versionData.deletion_time)
162
+ .map(([versionNum, versionData]: [string, any]) => ({
163
+ value: versionNum,
164
+ label: `${secretPath}:${versionNum}`,
165
+ createdTime: versionData.created_time
166
+ }))
167
+ .sort((a, b) => parseInt(b.value) - parseInt(a.value)) // Sort by version number descending
168
+ }
169
+
170
+ const toggleContentVisibility = () => {
171
+ setShowContent(!showContent)
172
+ }
173
+
174
+ const copyToClipboard = (value: string) => {
175
+ navigator.clipboard.writeText(value)
176
+ toast({
177
+ variant: "success",
178
+ title: "Copied to clipboard",
179
+ description: "Certificate content has been copied."
180
+ })
181
+ }
182
+
183
+ const handleUpdate = async () => {
184
+ if (!secretPath) {
185
+ onUpdate?.()
186
+ return
187
+ }
188
+
189
+ try {
190
+ const request: CreateSecretRequest | UpdateSecretRequest = {
191
+ data: createForm,
192
+ metadata: {
193
+ description: createForm.description || undefined
194
+ }
195
+ }
196
+
197
+ if (secretExists) {
198
+ await updateCertificate(secretPath, request as UpdateSecretRequest)
199
+ toast({
200
+ variant: "success",
201
+ title: "Certificate updated",
202
+ description: `Certificate "${secretPath}" has been updated successfully.`
203
+ })
204
+ } else {
205
+ await createCertificate(secretPath, request as CreateSecretRequest)
206
+ setSecretExists(true)
207
+ toast({
208
+ variant: "success",
209
+ title: "Certificate created",
210
+ description: `Certificate "${secretPath}" has been created successfully.`
211
+ })
212
+ }
213
+
214
+ // Reset change tracking after successful update
215
+ setOriginalContent(createForm.content)
216
+ setOriginalDescription(createForm.description)
217
+ setHasChanges(false)
218
+
219
+ onUpdate?.()
220
+ } catch (error) {
221
+ console.error("Error updating certificate:", error)
222
+ toast({
223
+ variant: "destructive",
224
+ title: "Error updating certificate",
225
+ description: "Failed to save certificate data."
226
+ })
227
+ }
228
+ }
229
+
230
+ if (loading) {
231
+ return (
232
+ <div className={`space-y-6 ${className}`}>
233
+ <div className="animate-pulse">
234
+ <div className="h-4 bg-ibm-gray-20 mb-4"></div>
235
+ <div className="h-32 bg-ibm-gray-20"></div>
236
+ </div>
237
+ </div>
238
+ )
239
+ }
240
+
241
+ return (
242
+ <div className={`space-y-6 ${className}`}>
243
+ {/* Certificate Section */}
244
+ <div>
245
+ <div className="flex items-center justify-between mb-4">
246
+ <Label className="text-sm font-medium text-ibm-gray-100">Secret file</Label>
247
+ {secretPath && (
248
+ <div className="flex items-center gap-3">
249
+ <Button
250
+ variant="ghost"
251
+ size="sm"
252
+ onClick={refreshCertificate}
253
+ style={{borderRadius: 0}}
254
+ leftIcon={<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />}
255
+ disabled={loading}
256
+ >
257
+ Refresh
258
+ </Button>
259
+
260
+ {/* Version Selector */}
261
+ {getAvailableVersions().length > 0 && (
262
+ <Select value={selectedVersion} onValueChange={handleVersionChange}>
263
+ <SelectTrigger className="w-64 h-12" style={{borderRadius: 0}}>
264
+ <SelectValue placeholder="Select a version" />
265
+ </SelectTrigger>
266
+ <SelectContent>
267
+ {getAvailableVersions().map((version) => (
268
+ <SelectItem key={version.value} value={version.value}>
269
+ <div className="flex flex-col">
270
+ <span className="font-medium">{version.label}</span>
271
+ <span className="text-xs text-ibm-gray-70">
272
+ {new Date(version.createdTime).toLocaleString()}
273
+ </span>
274
+ </div>
275
+ </SelectItem>
276
+ ))}
277
+ </SelectContent>
278
+ </Select>
279
+ )}
280
+ </div>
281
+ )}
282
+ </div>
283
+
284
+ <Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as "content" | "metadata")} className="w-full">
285
+ <div className="border-b border-ibm-gray-20">
286
+ <TabsList className="h-auto bg-transparent border-0 p-0 justify-start gap-0">
287
+ <TabsTrigger
288
+ value="content"
289
+ 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"
290
+ >
291
+ <FileText className="w-4 h-4" />
292
+ Content
293
+ </TabsTrigger>
294
+ <TabsTrigger
295
+ value="metadata"
296
+ 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"
297
+ >
298
+ <Info className="w-4 h-4" />
299
+ Metadata
300
+ </TabsTrigger>
301
+ </TabsList>
302
+ </div>
303
+
304
+ <TabsContent value="content" className="mt-4">
305
+ <div className="space-y-4">
306
+ <div>
307
+ <div className="flex items-center justify-between mb-2">
308
+ <div className="flex items-center gap-2">
309
+ <Label className="text-sm font-medium text-ibm-gray-100">Expiration date:</Label>
310
+ <Input
311
+ type= "date"
312
+ value={expirationDate ? expirationDate.toISOString().split('T')[0] : ""}
313
+ onChange={(e) => setExpirationDate(e.target.value ? new Date(e.target.value) : null)}
314
+ className="w-30 h-6 border-none text-sm"
315
+ />
316
+ </div>
317
+ <div className="flex items-center gap-2">
318
+ <Button
319
+ variant="ghost"
320
+ size="xs"
321
+ onClick={toggleContentVisibility}
322
+ leftIcon={showContent ? <EyeOff className="w-3 h-3" /> : <Eye className="w-3 h-3" />}
323
+ className="h-6 px-2"
324
+ >
325
+ {showContent ? "Hide" : "Show"}
326
+ </Button>
327
+ <Button
328
+ variant="ghost"
329
+ size="xs"
330
+ onClick={() => copyToClipboard(createForm.content)}
331
+ leftIcon={<Copy className="w-3 h-3" />}
332
+ className="h-6 px-2"
333
+ >
334
+ Copy
335
+ </Button>
336
+ </div>
337
+ </div>
338
+ <Textarea
339
+ value={createForm.content}
340
+ onChange={(e) => {
341
+
342
+ setCreateForm(prev => ({ ...prev, content: e.target.value }))
343
+ onContentChange?.(e.target.value)
344
+ }}
345
+ placeholder="-----BEGIN CERTIFICATE-----&#10;Certificate content here...&#10;-----END CERTIFICATE-----"
346
+ className="mt-2 min-h-[200px] max-h-64 font-mono text-sm resize-y"
347
+ readOnly={readOnly}
348
+ style={{
349
+ fontFamily: 'monospace',
350
+ fontSize: '12px',
351
+ lineHeight: '1.4'
352
+ }}
353
+ />
354
+ <div className="mt-2 flex items-center gap-2">
355
+ <FileImport
356
+ onFileContent={(content, fileName, fileType) => {
357
+ setCreateForm(prev => ({ ...prev, content: content, fileType: fileType}))
358
+ }}
359
+ acceptedTypes={["text/plain", "application/x-pem-file", "application/x-x509-ca-cert"]}
360
+ buttonText="Import content"
361
+ buttonVariant="ghost"
362
+ buttonSize="sm"
363
+ />
364
+ <p className="text-xs text-ibm-gray-70">
365
+ Paste your secret file content in text format. This can be a certificate, private key, or certificate chain.
366
+ </p>
367
+ </div>
368
+ </div>
369
+ </div>
370
+ </TabsContent>
371
+
372
+ <TabsContent value="metadata" className="mt-4">
373
+ <div className="space-y-4">
374
+ <div>
375
+ <Label className="text-sm font-medium text-ibm-gray-100">Certificate Metadata</Label>
376
+ <div className="mt-2 p-4 bg-ibm-gray-10 border border-ibm-gray-20 rounded">
377
+ {Object.keys(metadata).length > 0 ? (
378
+ <div className="space-y-3">
379
+ {Object.entries(metadata).map(([key, value]) => (
380
+ <div key={key} className="flex items-start gap-3">
381
+ <span className="text-sm font-medium text-ibm-gray-100 min-w-[120px] capitalize">
382
+ {key.replace(/_/g, ' ')}:
383
+ </span>
384
+ <span className="text-sm text-ibm-gray-70 break-all">
385
+ {typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value || 'N/A')}
386
+ </span>
387
+ </div>
388
+ ))}
389
+ </div>
390
+ ) : (
391
+ <div className="text-center py-6">
392
+ <Info className="w-8 h-8 text-ibm-gray-30 mx-auto mb-2" />
393
+ <p className="text-sm text-ibm-gray-70">
394
+ No metadata available for this certificate
395
+ </p>
396
+ </div>
397
+ )}
398
+ </div>
399
+ <p className="text-xs text-ibm-gray-70 mt-1">
400
+ This information is automatically generated by the certificate management system.
401
+ </p>
402
+ </div>
403
+
404
+ {/* Description Field */}
405
+ <div>
406
+ {readOnly ? (
407
+ <div className="p-3 bg-ibm-gray-10 border border-ibm-gray-20 rounded">
408
+ <p className="text-sm text-ibm-gray-70">
409
+ {createForm.description || "No description provided"}
410
+ </p>
411
+ </div>
412
+ ) : (
413
+ <Input
414
+ value={createForm.description}
415
+ onChange={(e) => {
416
+ setCreateForm(prev => ({ ...prev, description: e.target.value }))
417
+ onDescriptionChange?.(e.target.value)
418
+ }}
419
+ placeholder="Enter certificate description..."
420
+ className="text-sm border-ibm-gray-20"
421
+ />
422
+ )}
423
+ </div>
424
+ </div>
425
+ </TabsContent>
426
+ </Tabs>
427
+ </div>
428
+
429
+ {/* Update Button */}
430
+ {showUpdateButton && !readOnly && (
431
+ <div className="flex justify-end pt-4 border-b-0">
432
+ <Button
433
+ variant="primary"
434
+ style={{borderRadius: 0}}
435
+ size="sm"
436
+ onClick={handleUpdate}
437
+ disabled={!hasChanges || !createForm.content.trim()}
438
+ >
439
+ Update Certificate
440
+ </Button>
441
+ </div>
442
+ )}
443
+ </div>
444
+ )
445
+ }