@orsetra/shared-ui 1.5.9 → 1.5.11
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/user-panel.tsx +1 -1
- package/components/ui/file-chip.tsx +62 -0
- package/components/ui/resources-input.tsx +158 -0
- package/components/ui/secret-context.tsx +6 -1
- package/components/ui/secret-input.tsx +14 -9
- package/package.json +1 -1
- package/components/ui/secret-explorer.tsx +0 -274
|
@@ -92,7 +92,7 @@ export function UserPanel({ main_base_url }: UserPanelProps) {
|
|
|
92
92
|
</button>
|
|
93
93
|
{currentProject && organization && (
|
|
94
94
|
<a
|
|
95
|
-
href={`${main_base_url}/
|
|
95
|
+
href={`${main_base_url}/business-units/${currentProject.id}`}
|
|
96
96
|
onClick={close}
|
|
97
97
|
className="text-xs text-[#0f62fe] hover:underline"
|
|
98
98
|
>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { FileCode, X } from "lucide-react"
|
|
4
|
+
|
|
5
|
+
export interface FileChipProps {
|
|
6
|
+
file: { name: string; id?: string }
|
|
7
|
+
isActive: boolean
|
|
8
|
+
isEditing: boolean
|
|
9
|
+
disabled?: boolean
|
|
10
|
+
onSelect: () => void
|
|
11
|
+
onDelete: (e: React.MouseEvent) => void
|
|
12
|
+
onNameChange: (name: string) => void
|
|
13
|
+
onEditStart: () => void
|
|
14
|
+
onEditEnd: () => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function FileChip({ file, isActive, isEditing, disabled, onSelect, onDelete, onNameChange, onEditStart, onEditEnd }: FileChipProps) {
|
|
18
|
+
const showInput = isEditing || file.name === ""
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
onClick={onSelect}
|
|
23
|
+
className={`flex items-center gap-1 px-2 py-0.5 text-xs cursor-pointer border transition-colors shrink-0 ${
|
|
24
|
+
isActive
|
|
25
|
+
? "border-ibm-blue-60 bg-white text-ibm-blue-60"
|
|
26
|
+
: "border-ibm-gray-20 bg-white text-ibm-gray-70 hover:border-ibm-gray-40"
|
|
27
|
+
}`}
|
|
28
|
+
>
|
|
29
|
+
<FileCode className="w-3 h-3 shrink-0 flex-none" />
|
|
30
|
+
|
|
31
|
+
{showInput ? (
|
|
32
|
+
<input
|
|
33
|
+
autoFocus
|
|
34
|
+
type="text"
|
|
35
|
+
value={file.name}
|
|
36
|
+
onChange={(e) => onNameChange(e.target.value)}
|
|
37
|
+
onBlur={onEditEnd}
|
|
38
|
+
onClick={(e) => e.stopPropagation()}
|
|
39
|
+
placeholder="filename.yaml"
|
|
40
|
+
className="w-24 text-xs outline-none border-b border-ibm-blue-40 bg-transparent placeholder:text-ibm-gray-40 text-ibm-gray-100"
|
|
41
|
+
/>
|
|
42
|
+
) : (
|
|
43
|
+
<span
|
|
44
|
+
className="truncate max-w-[120px]"
|
|
45
|
+
onDoubleClick={(e) => { e.stopPropagation(); if (isActive) onEditStart() }}
|
|
46
|
+
>
|
|
47
|
+
{file.name}
|
|
48
|
+
</span>
|
|
49
|
+
)}
|
|
50
|
+
|
|
51
|
+
{!disabled && (
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
onClick={(e) => { e.stopPropagation(); onDelete(e) }}
|
|
55
|
+
className="ml-0.5 hover:text-red-500 transition-colors flex-none"
|
|
56
|
+
>
|
|
57
|
+
<X className="w-3 h-3" />
|
|
58
|
+
</button>
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react"
|
|
4
|
+
import { Button } from "./button"
|
|
5
|
+
import { Plus } from "lucide-react"
|
|
6
|
+
import { SecretInput } from "./secret-input"
|
|
7
|
+
import { FileChip } from "./file-chip"
|
|
8
|
+
|
|
9
|
+
interface ResourcesInputProps {
|
|
10
|
+
id?: string
|
|
11
|
+
value?: string[]
|
|
12
|
+
onChange?: (value: string[]) => void
|
|
13
|
+
disabled?: boolean
|
|
14
|
+
[key: string]: any
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const SECRET_PREFIX = "secret:"
|
|
18
|
+
const BASE_PATH = "/etc"
|
|
19
|
+
|
|
20
|
+
function parseItem(item: string): { name: string; path: string } {
|
|
21
|
+
const raw = item.startsWith(SECRET_PREFIX) ? item.slice(SECRET_PREFIX.length) : item
|
|
22
|
+
const at = raw.indexOf("@")
|
|
23
|
+
if (at === -1) return { name: raw, path: BASE_PATH }
|
|
24
|
+
return { name: raw.slice(0, at), path: raw.slice(at + 1) }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function buildItem(name: string, path: string): string {
|
|
28
|
+
return `${SECRET_PREFIX}${name}@${path}`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function pathSuffix(path: string): string {
|
|
32
|
+
return path.startsWith(BASE_PATH) ? path.slice(BASE_PATH.length) : path
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function ResourcesInput({ id, value = [], onChange, disabled = false, ...rest }: ResourcesInputProps) {
|
|
36
|
+
const [items, setItems] = useState<string[]>(value.filter(Boolean))
|
|
37
|
+
const [draft, setDraft] = useState("")
|
|
38
|
+
const [editingNameIndex, setEditingNameIndex] = useState<number | null>(null)
|
|
39
|
+
const [editingPathIndex, setEditingPathIndex] = useState<number | null>(null)
|
|
40
|
+
const [editingPathSuffix, setEditingPathSuffix] = useState("")
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const filtered = value.filter(Boolean)
|
|
44
|
+
if (JSON.stringify(filtered) !== JSON.stringify(items)) {
|
|
45
|
+
setItems(filtered)
|
|
46
|
+
}
|
|
47
|
+
}, [value])
|
|
48
|
+
|
|
49
|
+
const handleAdd = () => {
|
|
50
|
+
if (!draft || items.includes(draft)) return
|
|
51
|
+
const next = [...items, draft]
|
|
52
|
+
setItems(next)
|
|
53
|
+
onChange?.(next)
|
|
54
|
+
setDraft("")
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const handleRemove = (index: number) => {
|
|
58
|
+
const next = items.filter((_, i) => i !== index)
|
|
59
|
+
setItems(next)
|
|
60
|
+
onChange?.(next)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const handleNameChange = (index: number, newName: string) => {
|
|
64
|
+
const { path } = parseItem(items[index])
|
|
65
|
+
const next = items.map((item, i) => i === index ? buildItem(newName, path) : item)
|
|
66
|
+
setItems(next)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const handleNameEditEnd = (index: number) => {
|
|
70
|
+
setEditingNameIndex(null)
|
|
71
|
+
onChange?.(items)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const startPathEditing = (index: number, path: string) => {
|
|
75
|
+
if (disabled) return
|
|
76
|
+
setEditingPathIndex(index)
|
|
77
|
+
setEditingPathSuffix(pathSuffix(path))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const confirmPathEdit = (index: number) => {
|
|
81
|
+
const { name } = parseItem(items[index])
|
|
82
|
+
const fullPath = `${BASE_PATH}${editingPathSuffix}`
|
|
83
|
+
const next = items.map((item, i) => i === index ? buildItem(name, fullPath) : item)
|
|
84
|
+
setItems(next)
|
|
85
|
+
onChange?.(next)
|
|
86
|
+
setEditingPathIndex(null)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div className="space-y-2">
|
|
91
|
+
{items.map((item, index) => {
|
|
92
|
+
const { name, path } = parseItem(item)
|
|
93
|
+
const suffix = pathSuffix(path)
|
|
94
|
+
return (
|
|
95
|
+
<div key={index} className="flex items-center gap-2">
|
|
96
|
+
<FileChip
|
|
97
|
+
file={{ name, id: String(index) }}
|
|
98
|
+
isActive={editingNameIndex === index}
|
|
99
|
+
isEditing={editingNameIndex === index}
|
|
100
|
+
disabled={disabled}
|
|
101
|
+
onSelect={() => {}}
|
|
102
|
+
onDelete={(e) => handleRemove(index)}
|
|
103
|
+
onNameChange={(newName) => handleNameChange(index, newName)}
|
|
104
|
+
onEditStart={() => setEditingNameIndex(index)}
|
|
105
|
+
onEditEnd={() => handleNameEditEnd(index)}
|
|
106
|
+
/>
|
|
107
|
+
|
|
108
|
+
{editingPathIndex === index ? (
|
|
109
|
+
<div className="flex flex-1 items-center border border-ibm-blue-60 bg-white min-w-0 h-10">
|
|
110
|
+
<span className="px-2 text-sm font-mono text-ibm-gray-40 shrink-0 select-none">{BASE_PATH}</span>
|
|
111
|
+
<input
|
|
112
|
+
autoFocus
|
|
113
|
+
value={editingPathSuffix}
|
|
114
|
+
onChange={(e) => setEditingPathSuffix(e.target.value)}
|
|
115
|
+
onBlur={() => confirmPathEdit(index)}
|
|
116
|
+
onKeyDown={(e) => {
|
|
117
|
+
if (e.key === "Enter") confirmPathEdit(index)
|
|
118
|
+
if (e.key === "Escape") setEditingPathIndex(null)
|
|
119
|
+
}}
|
|
120
|
+
className="flex-1 text-sm font-mono text-ibm-gray-100 bg-transparent focus:outline-none pr-2 min-w-0"
|
|
121
|
+
placeholder="/myapp"
|
|
122
|
+
/>
|
|
123
|
+
</div>
|
|
124
|
+
) : (
|
|
125
|
+
<span
|
|
126
|
+
onDoubleClick={() => startPathEditing(index, path)}
|
|
127
|
+
title={disabled ? undefined : "Double-click to edit"}
|
|
128
|
+
className={`flex flex-1 items-center border border-ibm-gray-20 bg-white h-10 px-3 font-mono text-sm truncate min-w-0 ${
|
|
129
|
+
disabled ? "text-ibm-gray-40" : "cursor-text hover:border-ibm-gray-40"
|
|
130
|
+
}`}
|
|
131
|
+
>
|
|
132
|
+
<span className="text-ibm-gray-40 shrink-0">{BASE_PATH}</span>
|
|
133
|
+
<span className={suffix ? "text-ibm-gray-70" : "text-ibm-gray-40 italic"}>
|
|
134
|
+
{suffix || "/…"}
|
|
135
|
+
</span>
|
|
136
|
+
</span>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
)
|
|
140
|
+
})}
|
|
141
|
+
|
|
142
|
+
<div className="flex gap-2">
|
|
143
|
+
<div className="flex-1">
|
|
144
|
+
<SecretInput value={draft} onChange={setDraft} disabled={disabled} type={id} />
|
|
145
|
+
</div>
|
|
146
|
+
<Button
|
|
147
|
+
type="button"
|
|
148
|
+
variant="secondary"
|
|
149
|
+
onClick={handleAdd}
|
|
150
|
+
disabled={disabled || !draft}
|
|
151
|
+
className="rounded-none h-10 w-10 p-0 shrink-0"
|
|
152
|
+
>
|
|
153
|
+
<Plus className="h-3.5 w-3.5" />
|
|
154
|
+
</Button>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
@@ -6,23 +6,28 @@ import type { ReactNode } from "react"
|
|
|
6
6
|
export interface SecretEntry {
|
|
7
7
|
name: string
|
|
8
8
|
description?: string
|
|
9
|
+
labels?: string[]
|
|
10
|
+
template?: string
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
interface SecretContextValue {
|
|
12
14
|
secretsMap: Map<string, SecretEntry[]>
|
|
15
|
+
loading: boolean
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
const SecretContext = createContext<SecretContextValue | null>(null)
|
|
16
19
|
|
|
17
20
|
export function SecretContextProvider({
|
|
18
21
|
children,
|
|
22
|
+
loading = false,
|
|
19
23
|
secretsMap,
|
|
20
24
|
}: {
|
|
21
25
|
children: ReactNode
|
|
26
|
+
loading?: boolean
|
|
22
27
|
secretsMap: Map<string, SecretEntry[]>
|
|
23
28
|
}) {
|
|
24
29
|
return (
|
|
25
|
-
<SecretContext.Provider value={{ secretsMap }}>
|
|
30
|
+
<SecretContext.Provider value={{ secretsMap, loading }}>
|
|
26
31
|
{children}
|
|
27
32
|
</SecretContext.Provider>
|
|
28
33
|
)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { SelectInput } from "./select-input"
|
|
4
4
|
import { useSecretContext } from "./secret-context"
|
|
5
|
+
import { Loader2 } from "lucide-react"
|
|
5
6
|
|
|
6
7
|
interface SecretInputProps {
|
|
7
8
|
id?: string
|
|
@@ -31,16 +32,20 @@ export function SecretInput({
|
|
|
31
32
|
</div>
|
|
32
33
|
)
|
|
33
34
|
}
|
|
35
|
+
|
|
34
36
|
const secrets = ctx.secretsMap.get(type || 'configs') || []
|
|
35
37
|
return (
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
38
|
+
<div className="flex items-center gap-2">
|
|
39
|
+
<SelectInput
|
|
40
|
+
id={id}
|
|
41
|
+
value={value}
|
|
42
|
+
onChange={onChange}
|
|
43
|
+
disabled={disabled || ctx.loading}
|
|
44
|
+
className={className}
|
|
45
|
+
placeholder={ctx.loading ? "Loading…" : placeholder}
|
|
46
|
+
options={secrets.map((s) => ({ label: s.description || s.name, value: s.name }))}
|
|
47
|
+
/>
|
|
48
|
+
{ctx.loading && <Loader2 className="h-4 w-4 animate-spin text-ibm-blue-60 flex-shrink-0" />}
|
|
49
|
+
</div>
|
|
45
50
|
)
|
|
46
51
|
}
|
package/package.json
CHANGED
|
@@ -1,274 +0,0 @@
|
|
|
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
|
-
}
|