@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.
- 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 +10 -1
- package/package.json +35 -8
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react"
|
|
4
|
+
import { Button } from "./button"
|
|
5
|
+
import { Card, CardContent, CardHeader, CardTitle } from "./card"
|
|
6
|
+
import { Key, Plus, Trash2, Folder, File, ChevronRight } from "lucide-react"
|
|
7
|
+
import { useToast } from "../../hooks/use-toast"
|
|
8
|
+
|
|
9
|
+
import type { Secrets } from "@/app/secrets/models"
|
|
10
|
+
|
|
11
|
+
interface SecretExplorerProps {
|
|
12
|
+
currentPath: string
|
|
13
|
+
onPathChange: (path: string) => void
|
|
14
|
+
getSecrets: (path: string) => Promise<Secrets>
|
|
15
|
+
deleteSecret: (path: string) => Promise<boolean>
|
|
16
|
+
selectedSecret: string | null
|
|
17
|
+
onSecretSelect: (secret: string | null) => void
|
|
18
|
+
onCreateClick: () => void
|
|
19
|
+
onRefresh?: () => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function SecretExplorer({
|
|
23
|
+
currentPath,
|
|
24
|
+
onPathChange,
|
|
25
|
+
getSecrets,
|
|
26
|
+
deleteSecret,
|
|
27
|
+
selectedSecret,
|
|
28
|
+
onSecretSelect,
|
|
29
|
+
onCreateClick,
|
|
30
|
+
onRefresh
|
|
31
|
+
}: SecretExplorerProps) {
|
|
32
|
+
const { toast } = useToast()
|
|
33
|
+
const [secretPaths, setSecretPaths] = useState<string[]>([])
|
|
34
|
+
const [loading, setLoading] = useState(true)
|
|
35
|
+
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
|
36
|
+
const [selectedSecretForAction, setSelectedSecretForAction] = useState<string | null>(null)
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
loadSecrets()
|
|
40
|
+
}, [currentPath])
|
|
41
|
+
|
|
42
|
+
const loadSecrets = async () => {
|
|
43
|
+
try {
|
|
44
|
+
const secretsResponse: Secrets = await getSecrets(currentPath || "/")
|
|
45
|
+
const paths = secretsResponse.data?.keys || []
|
|
46
|
+
setSecretPaths(paths)
|
|
47
|
+
} catch (error) {
|
|
48
|
+
// Navigate up if current path is invalid
|
|
49
|
+
if (currentPath) {
|
|
50
|
+
const pathParts = currentPath.split('/').filter(Boolean)
|
|
51
|
+
pathParts.pop()
|
|
52
|
+
const newPath = pathParts.length > 0 ? pathParts.join('/') + '/' : ""
|
|
53
|
+
onPathChange(newPath)
|
|
54
|
+
}
|
|
55
|
+
toast({
|
|
56
|
+
variant: "destructive",
|
|
57
|
+
title: "Error loading secrets",
|
|
58
|
+
description: "Failed to fetch configuration secrets."
|
|
59
|
+
})
|
|
60
|
+
} finally {
|
|
61
|
+
setLoading(false)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const handleItemClick = async (path: string) => {
|
|
66
|
+
if (path.endsWith('/')) {
|
|
67
|
+
// It's a folder, navigate into it
|
|
68
|
+
const newPath = currentPath ? `${currentPath}${path}` : path
|
|
69
|
+
onPathChange(newPath)
|
|
70
|
+
onSecretSelect(null)
|
|
71
|
+
} else {
|
|
72
|
+
// It's a secret, show details
|
|
73
|
+
onSecretSelect(path)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const getBreadcrumbs = () => {
|
|
78
|
+
if (!currentPath) return [{ name: "Root", path: "" }]
|
|
79
|
+
|
|
80
|
+
const parts = currentPath.split('/').filter(Boolean)
|
|
81
|
+
const breadcrumbs = [{ name: "Root", path: "" }]
|
|
82
|
+
|
|
83
|
+
let accumulatedPath = ""
|
|
84
|
+
parts.forEach((part, index) => {
|
|
85
|
+
accumulatedPath += part + "/"
|
|
86
|
+
breadcrumbs.push({
|
|
87
|
+
name: part,
|
|
88
|
+
path: accumulatedPath
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
return breadcrumbs
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const handleDeleteSecret = async () => {
|
|
96
|
+
if (!selectedSecretForAction) return
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const fullPath = currentPath ? `${currentPath}${selectedSecretForAction}` : selectedSecretForAction
|
|
100
|
+
await deleteSecret(fullPath)
|
|
101
|
+
|
|
102
|
+
toast({
|
|
103
|
+
variant: "success",
|
|
104
|
+
title: "Secret deleted successfully",
|
|
105
|
+
description: `Configuration "${selectedSecretForAction}" has been deleted.`
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
setShowDeleteDialog(false)
|
|
109
|
+
setSelectedSecretForAction(null)
|
|
110
|
+
loadSecrets()
|
|
111
|
+
|
|
112
|
+
// Clear details if this was the selected secret
|
|
113
|
+
if (selectedSecret === selectedSecretForAction) {
|
|
114
|
+
onSecretSelect(null)
|
|
115
|
+
}
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error("Error deleting secret:", error)
|
|
118
|
+
toast({
|
|
119
|
+
variant: "destructive",
|
|
120
|
+
title: "Error deleting secret",
|
|
121
|
+
description: "Failed to delete configuration secret."
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (loading) {
|
|
127
|
+
return (
|
|
128
|
+
<Card className="border border-ibm-gray-20">
|
|
129
|
+
<CardHeader className="pb-3">
|
|
130
|
+
<div className="animate-pulse">
|
|
131
|
+
<div className="h-6 bg-ibm-gray-20 mb-2"></div>
|
|
132
|
+
<div className="h-4 bg-ibm-gray-20"></div>
|
|
133
|
+
</div>
|
|
134
|
+
</CardHeader>
|
|
135
|
+
<CardContent className="p-0">
|
|
136
|
+
<div className="animate-pulse">
|
|
137
|
+
<div className="h-32 bg-ibm-gray-20"></div>
|
|
138
|
+
</div>
|
|
139
|
+
</CardContent>
|
|
140
|
+
</Card>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<>
|
|
146
|
+
<Card className="border border-ibm-gray-20">
|
|
147
|
+
<CardHeader className="pb-3">
|
|
148
|
+
<div className="flex items-center justify-between">
|
|
149
|
+
<CardTitle className="text-lg font-medium">Secrets Explorer</CardTitle>
|
|
150
|
+
<Button variant="ghost" size="sm" onClick={onCreateClick}>
|
|
151
|
+
<Plus className="w-4 h-4" />
|
|
152
|
+
</Button>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{/* Breadcrumbs */}
|
|
156
|
+
<div className="flex items-center gap-1 text-xs text-ibm-gray-70 mt-2">
|
|
157
|
+
{getBreadcrumbs().map((crumb, index) => (
|
|
158
|
+
<div key={crumb.path} className="flex items-center gap-1">
|
|
159
|
+
{index > 0 && <ChevronRight className="w-3 h-3" />}
|
|
160
|
+
<button
|
|
161
|
+
onClick={() => {
|
|
162
|
+
onPathChange(crumb.path)
|
|
163
|
+
onSecretSelect(null)
|
|
164
|
+
}}
|
|
165
|
+
className={`hover:text-ibm-blue-60 ${
|
|
166
|
+
crumb.path === currentPath ? "text-ibm-blue-60 font-medium" : ""
|
|
167
|
+
}`}
|
|
168
|
+
>
|
|
169
|
+
{crumb.name}
|
|
170
|
+
</button>
|
|
171
|
+
</div>
|
|
172
|
+
))}
|
|
173
|
+
</div>
|
|
174
|
+
</CardHeader>
|
|
175
|
+
|
|
176
|
+
<CardContent className="p-0">
|
|
177
|
+
{/* File List */}
|
|
178
|
+
<div className="border-t border-ibm-gray-20">
|
|
179
|
+
{secretPaths.length === 0 ? (
|
|
180
|
+
<div className="p-8 text-center">
|
|
181
|
+
<Key className="w-8 h-8 text-ibm-gray-30 mx-auto mb-2" />
|
|
182
|
+
<p className="text-sm text-ibm-gray-70">
|
|
183
|
+
Empty folder
|
|
184
|
+
</p>
|
|
185
|
+
</div>
|
|
186
|
+
) : (
|
|
187
|
+
<div className="divide-y divide-ibm-gray-20">
|
|
188
|
+
{secretPaths.map((path) => {
|
|
189
|
+
const isFolder = path.endsWith('/')
|
|
190
|
+
const displayName = isFolder ? path.slice(0, -1) : path
|
|
191
|
+
const isSelected = selectedSecret === path
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<div
|
|
195
|
+
key={path}
|
|
196
|
+
className={`group flex items-center gap-3 px-4 py-3 hover:bg-ibm-gray-10 cursor-pointer transition-colors ${
|
|
197
|
+
isSelected ? "bg-ibm-blue-10 border-r-2 border-ibm-blue-60" : ""
|
|
198
|
+
}`}
|
|
199
|
+
onClick={() => handleItemClick(path)}
|
|
200
|
+
>
|
|
201
|
+
<div className="flex-shrink-0">
|
|
202
|
+
{isFolder ? (
|
|
203
|
+
<Folder className="w-4 h-4 text-ibm-yellow-60" />
|
|
204
|
+
) : (
|
|
205
|
+
<File className="w-4 h-4 text-ibm-blue-60" />
|
|
206
|
+
)}
|
|
207
|
+
</div>
|
|
208
|
+
<span className="text-sm text-ibm-gray-100 flex-1 truncate">
|
|
209
|
+
{displayName}
|
|
210
|
+
</span>
|
|
211
|
+
|
|
212
|
+
{!isFolder && (
|
|
213
|
+
<Button
|
|
214
|
+
variant="ghost"
|
|
215
|
+
size="xs"
|
|
216
|
+
leftIcon={<Trash2 className="w-3 h-3" />}
|
|
217
|
+
onClick={(e) => {
|
|
218
|
+
e.stopPropagation()
|
|
219
|
+
setSelectedSecretForAction(path)
|
|
220
|
+
setShowDeleteDialog(true)
|
|
221
|
+
}}
|
|
222
|
+
className="opacity-0 group-hover:opacity-100 transition-opacity text-ibm-red-60 hover:text-ibm-red-70 hover:bg-ibm-red-10"
|
|
223
|
+
>
|
|
224
|
+
</Button>
|
|
225
|
+
)}
|
|
226
|
+
</div>
|
|
227
|
+
)
|
|
228
|
+
})}
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
</CardContent>
|
|
233
|
+
</Card>
|
|
234
|
+
|
|
235
|
+
{/* Delete Confirmation Dialog */}
|
|
236
|
+
<div
|
|
237
|
+
className={`fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 ${
|
|
238
|
+
showDeleteDialog ? 'block' : 'hidden'
|
|
239
|
+
}`}
|
|
240
|
+
onClick={() => setShowDeleteDialog(false)}
|
|
241
|
+
>
|
|
242
|
+
<div
|
|
243
|
+
className="bg-white p-6 rounded-lg max-w-md w-full mx-4"
|
|
244
|
+
onClick={(e) => e.stopPropagation()}
|
|
245
|
+
>
|
|
246
|
+
<h3 className="text-lg font-medium text-ibm-gray-100 mb-2">Delete Secret</h3>
|
|
247
|
+
<p className="text-sm text-ibm-gray-70 mb-6">
|
|
248
|
+
Are you sure you want to delete the secret "{selectedSecretForAction}"?
|
|
249
|
+
This action cannot be undone and will permanently remove all secret data.
|
|
250
|
+
</p>
|
|
251
|
+
<div className="flex justify-end gap-3">
|
|
252
|
+
<Button
|
|
253
|
+
variant="secondary"
|
|
254
|
+
size="sm"
|
|
255
|
+
style={{ borderRadius: 0 }}
|
|
256
|
+
onClick={() => setShowDeleteDialog(false)}
|
|
257
|
+
>
|
|
258
|
+
Cancel
|
|
259
|
+
</Button>
|
|
260
|
+
<Button
|
|
261
|
+
variant="danger"
|
|
262
|
+
style={{ borderRadius: 0 }}
|
|
263
|
+
leftIcon={<Trash2 className="w-3 h-3" />}
|
|
264
|
+
size="sm"
|
|
265
|
+
onClick={handleDeleteSecret}
|
|
266
|
+
>
|
|
267
|
+
Delete Secret
|
|
268
|
+
</Button>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
</>
|
|
273
|
+
)
|
|
274
|
+
}
|