@orsetra/shared-ui 1.5.10 → 1.5.12

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.
@@ -56,11 +56,12 @@ export function MainSidebar({
56
56
  onSecondarySidebarOpen()
57
57
  }
58
58
 
59
- const rawUrl = item.href ?? `${main_base_url}/${menuId}`
60
- const path = rawUrl.startsWith('http://') || rawUrl.startsWith('https://')
61
- ? new URL(rawUrl).pathname
62
- : rawUrl
63
- window.location.href = path
59
+ if (item.href) {
60
+ const path = item.href.startsWith('http://') || item.href.startsWith('https://')
61
+ ? new URL(item.href).pathname
62
+ : item.href
63
+ window.location.href = path
64
+ }
64
65
 
65
66
  if (!isMinimized) {
66
67
  onToggle()
@@ -126,34 +127,58 @@ export function MainSidebar({
126
127
  const isActive = currentMenu === item.id
127
128
  const isExpanded = expandedMenu === item.id
128
129
 
130
+ const itemHref = !hasSubMenu && item.href
131
+ ? (item.href.startsWith('http://') || item.href.startsWith('https://')
132
+ ? new URL(item.href).pathname
133
+ : item.href)
134
+ : undefined
135
+
136
+ const itemClassName = cn(
137
+ "w-full flex items-center text-left transition-all duration-150 font-medium",
138
+ isMinimized ? "justify-center p-3" : "gap-3 px-3 py-2",
139
+ isActive
140
+ ? "bg-interactive/10 text-interactive border-l-4 border-interactive"
141
+ : "text-text-primary hover:bg-ui-background border-l-4 border-transparent"
142
+ )
143
+
144
+ const itemContent = (
145
+ <>
146
+ <Icon className={cn(
147
+ "h-5 w-5 flex-shrink-0",
148
+ isActive ? "text-interactive" : "text-text-secondary"
149
+ )} />
150
+ {!isMinimized && (
151
+ <>
152
+ <span className="text-base flex-1">{item.label}</span>
153
+ {hasSubMenu && (
154
+ isExpanded
155
+ ? <ChevronDown className="h-4 w-4 text-text-secondary flex-shrink-0" />
156
+ : <ChevronRight className="h-4 w-4 text-text-secondary flex-shrink-0" />
157
+ )}
158
+ </>
159
+ )}
160
+ </>
161
+ )
162
+
129
163
  return (
130
164
  <div key={item.id}>
131
- <button
132
- onClick={() => handleMenuClick(item)}
133
- className={cn(
134
- "w-full flex items-center text-left transition-all duration-150 font-medium",
135
- isMinimized ? "justify-center p-3" : "gap-3 px-3 py-2",
136
- isActive
137
- ? "bg-interactive/10 text-interactive border-l-4 border-interactive"
138
- : "text-text-primary hover:bg-ui-background border-l-4 border-transparent"
139
- )}
140
- title={isMinimized ? item.label : undefined}
141
- >
142
- <Icon className={cn(
143
- "h-5 w-5 flex-shrink-0",
144
- isActive ? "text-interactive" : "text-text-secondary"
145
- )} />
146
- {!isMinimized && (
147
- <>
148
- <span className="text-base flex-1">{item.label}</span>
149
- {hasSubMenu && (
150
- isExpanded
151
- ? <ChevronDown className="h-4 w-4 text-text-secondary flex-shrink-0" />
152
- : <ChevronRight className="h-4 w-4 text-text-secondary flex-shrink-0" />
153
- )}
154
- </>
155
- )}
156
- </button>
165
+ {itemHref ? (
166
+ <a
167
+ href={itemHref}
168
+ className={itemClassName}
169
+ title={isMinimized ? item.label : undefined}
170
+ >
171
+ {itemContent}
172
+ </a>
173
+ ) : (
174
+ <button
175
+ onClick={() => handleMenuClick(item)}
176
+ className={itemClassName}
177
+ title={isMinimized ? item.label : undefined}
178
+ >
179
+ {itemContent}
180
+ </button>
181
+ )}
157
182
 
158
183
  {/* Inline accordion sub-items */}
159
184
  {!isMinimized && isExpanded && hasSubMenu && (
@@ -197,9 +197,9 @@ function Sidebar({ currentMenu, onMainMenuToggle, sidebarMenus = {}, main_base_u
197
197
 
198
198
  const filteredMainItems = React.useMemo(() => {
199
199
  if (!isCollapsed || mainMenuItems.length === 0) return []
200
- // Exclude items that match the current menu context OR appear in the secondary nav
200
+ // Exclude items that match the current menu context OR appear in the secondary nav OR have no href
201
201
  const navIds = new Set(currentNavigation.map((n) => n.id))
202
- return mainMenuItems.filter((item) => item.id !== currentMenu && !navIds.has(item.id))
202
+ return mainMenuItems.filter((item) => item.id !== currentMenu && !navIds.has(item.id) && !!item.href)
203
203
  }, [isCollapsed, mainMenuItems, currentNavigation, currentMenu])
204
204
 
205
205
  return (
@@ -300,8 +300,9 @@ function Sidebar({ currentMenu, onMainMenuToggle, sidebarMenus = {}, main_base_u
300
300
  <>
301
301
  <div className="border-t border-ui-border mx-1 my-2" />
302
302
  {filteredMainItems.map((item) => {
303
- const rawHref = item.href || `${main_base_url}/${item.id}`
304
- const href = rawHref.startsWith('http') ? new URL(rawHref).pathname : rawHref
303
+ const href = item.href!.startsWith('http://') || item.href!.startsWith('https://')
304
+ ? new URL(item.href!).pathname
305
+ : item.href!
305
306
  const linkEl = (
306
307
  <Link
307
308
  key={item.id}
@@ -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
+ }
@@ -64,6 +64,7 @@ export { ToggleGroup, ToggleGroupItem } from './toggle-group'
64
64
  export { Toggle } from './toggle'
65
65
  export { StringsInput } from './strings-input'
66
66
  export { KVInput } from './kv-input'
67
+ export { ResourcesInput } from './resources-input'
67
68
  export { NumbersInput } from './numbers-input'
68
69
  export { KVDynamicInput } from './kv-dynamic-input'
69
70
  export { SelectInput } from './select-input'
@@ -73,4 +74,5 @@ export { SecretsInput } from './secrets-input'
73
74
  export { StructsInput } from './structs-input'
74
75
  export { CodeEditor } from './code-editor'
75
76
  export { K8sObjectsCode } from './k8s-objects-code'
77
+ export { FileChip } from './file-chip'
76
78
  export { EnvironmentPickerDialog, type EnvironmentPickerDialogProps } from './environment-picker-dialog'
@@ -0,0 +1,170 @@
1
+ "use client"
2
+
3
+ import { useState, useEffect, useRef } from "react"
4
+ import { Button } from "./button"
5
+ import { Plus, X, FileCode } from "lucide-react"
6
+ import { SecretInput } from "./secret-input"
7
+
8
+ interface ResourcesInputProps {
9
+ id?: string
10
+ value?: string[]
11
+ onChange?: (value: string[]) => void
12
+ disabled?: boolean
13
+ [key: string]: any
14
+ }
15
+
16
+ const SECRET_PREFIX = "secret:"
17
+ const BASE_PATH = "/etc"
18
+
19
+ function parseItem(item: string): { name: string; path: string } {
20
+ const raw = item.startsWith(SECRET_PREFIX) ? item.slice(SECRET_PREFIX.length) : item
21
+ const at = raw.indexOf("@")
22
+ if (at === -1) return { name: raw, path: BASE_PATH }
23
+ return { name: raw.slice(0, at), path: raw.slice(at + 1) }
24
+ }
25
+
26
+ function buildItem(name: string, path: string): string {
27
+ return `${SECRET_PREFIX}${name}@${path}`
28
+ }
29
+
30
+ function pathSuffix(path: string): string {
31
+ const after = path.startsWith(BASE_PATH) ? path.slice(BASE_PATH.length) : path
32
+ return after.startsWith("/") ? after.slice(1) : after
33
+ }
34
+
35
+ export function ResourcesInput({ id, value, onChange, disabled = false, ...rest }: ResourcesInputProps) {
36
+ const normalize = (v: any): string[] =>
37
+ Array.isArray(v) ? v.filter(Boolean).map((item) => item.startsWith(SECRET_PREFIX) ? item : buildItem(parseItem(item).name, parseItem(item).path)) : []
38
+ const [items, setItems] = useState<string[]>(normalize(value))
39
+ const [draft, setDraft] = useState("")
40
+ const [editingPathIndex, setEditingPathIndex] = useState<number | null>(null)
41
+ const [editingPathSuffix, setEditingPathSuffix] = useState("")
42
+ const mounted = useRef(false)
43
+
44
+ useEffect(() => {
45
+ const filtered = normalize(value)
46
+ if (!mounted.current) {
47
+ mounted.current = true
48
+ const raw = Array.isArray(value) ? value.filter(Boolean) : []
49
+ if (JSON.stringify(filtered) !== JSON.stringify(raw)) {
50
+ onChange?.(filtered)
51
+ }
52
+ return
53
+ }
54
+ if (JSON.stringify(filtered) !== JSON.stringify(items)) {
55
+ setItems(filtered)
56
+ }
57
+ }, [value])
58
+
59
+ const handleAdd = () => {
60
+ if (!draft || items.includes(draft)) return
61
+ const next = [...items, draft]
62
+ setItems(next)
63
+ onChange?.(next)
64
+ setDraft("")
65
+ }
66
+
67
+ const handleRemove = (index: number) => {
68
+ const next = items.filter((_, i) => i !== index)
69
+ setItems(next)
70
+ onChange?.(next)
71
+ }
72
+
73
+ const startPathEditing = (index: number, path: string) => {
74
+ if (disabled) return
75
+ setEditingPathIndex(index)
76
+ setEditingPathSuffix(pathSuffix(path))
77
+ }
78
+
79
+ const confirmPathEdit = (index: number) => {
80
+ const { name } = parseItem(items[index])
81
+ const fullPath = `${BASE_PATH}/${editingPathSuffix}`
82
+ const next = items.map((item, i) => i === index ? buildItem(name, fullPath) : item)
83
+ setItems(next)
84
+ onChange?.(next)
85
+ setEditingPathIndex(null)
86
+ }
87
+
88
+ return (
89
+ <div className="space-y-2">
90
+ {items.map((item, index) => {
91
+ const { name, path } = parseItem(item)
92
+ const suffix = pathSuffix(path)
93
+ const isEditingPath = editingPathIndex === index
94
+
95
+ return (
96
+ <div
97
+ key={index}
98
+ className={`flex items-center h-10 bg-white transition-colors ${
99
+ isEditingPath ? "" : "border border-ibm-gray-20 hover:border-ibm-gray-40"
100
+ }`}
101
+ >
102
+ {/* Name */}
103
+ <div className="flex items-center gap-1.5 px-3 shrink-0 border-r border-ibm-gray-20">
104
+ <FileCode className="w-3 h-3 text-ibm-gray-50 shrink-0" />
105
+ <span className="text-xs font-mono text-ibm-gray-100">{name}</span>
106
+ </div>
107
+
108
+ {/* Path */}
109
+ {isEditingPath ? (
110
+ <div className="flex flex-1 items-center px-3 min-w-0">
111
+ <span className="text-sm font-mono text-ibm-gray-40 shrink-0 select-none">{BASE_PATH}/</span>
112
+ <input
113
+ autoFocus
114
+ value={editingPathSuffix}
115
+ onChange={(e) => setEditingPathSuffix(e.target.value)}
116
+ onBlur={() => confirmPathEdit(index)}
117
+ onKeyDown={(e) => {
118
+ if (e.key === "Enter") confirmPathEdit(index)
119
+ if (e.key === "Escape") setEditingPathIndex(null)
120
+ }}
121
+ className="flex-1 text-sm font-mono text-ibm-gray-100 bg-transparent focus:outline-none min-w-0"
122
+ placeholder="/myapp"
123
+ />
124
+ </div>
125
+ ) : (
126
+ <span
127
+ onDoubleClick={() => startPathEditing(index, path)}
128
+ title={disabled ? undefined : "Double-click to edit path"}
129
+ className={`flex flex-1 items-center px-3 min-w-0 text-sm font-mono ${
130
+ disabled ? "cursor-default" : "cursor-text"
131
+ }`}
132
+ >
133
+ <span className="text-ibm-gray-40 shrink-0">{BASE_PATH}/</span>
134
+ <span className={`truncate ${suffix ? "text-ibm-gray-70" : "text-ibm-gray-40 italic"}`}>
135
+ {suffix || "…"}
136
+ </span>
137
+ </span>
138
+ )}
139
+
140
+ {/* Delete */}
141
+ {!disabled && (
142
+ <button
143
+ type="button"
144
+ onClick={() => handleRemove(index)}
145
+ className="px-2 h-full flex items-center text-ibm-gray-40 hover:text-red-500 transition-colors shrink-0 border-l border-ibm-gray-20"
146
+ >
147
+ <X className="w-3.5 h-3.5" />
148
+ </button>
149
+ )}
150
+ </div>
151
+ )
152
+ })}
153
+
154
+ <div className="flex gap-2">
155
+ <div className="flex-1">
156
+ <SecretInput value={draft} onChange={setDraft} disabled={disabled} type={id} />
157
+ </div>
158
+ <Button
159
+ type="button"
160
+ variant="secondary"
161
+ onClick={handleAdd}
162
+ disabled={disabled || !draft}
163
+ className="rounded-none h-10 w-10 p-0 shrink-0"
164
+ >
165
+ <Plus className="h-3.5 w-3.5" />
166
+ </Button>
167
+ </div>
168
+ </div>
169
+ )
170
+ }
@@ -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
+ console.log(type)
34
36
  const secrets = ctx.secretsMap.get(type || 'configs') || []
35
37
  return (
36
- <SelectInput
37
- id={id}
38
- value={value}
39
- onChange={onChange}
40
- disabled={disabled}
41
- className={className}
42
- placeholder={placeholder}
43
- options={secrets.map((s) => ({ label: s.description || s.name, value: `secret:${s.name}` }))}
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
  }
@@ -1,8 +1,8 @@
1
1
  "use client"
2
2
 
3
- import { useState, useEffect } from "react"
3
+ import { useState, useEffect, useRef } from "react"
4
4
  import { Button } from "./button"
5
- import { X, Plus } from "lucide-react"
5
+ import { X, Plus, FileCode } from "lucide-react"
6
6
  import { SecretInput } from "./secret-input"
7
7
 
8
8
  interface SecretsInputProps {
@@ -13,20 +13,43 @@ interface SecretsInputProps {
13
13
  [key: string]: any
14
14
  }
15
15
 
16
- export function SecretsInput({ id, value = [], onChange, disabled = false, ...rest }: SecretsInputProps) {
17
- const [items, setItems] = useState<string[]>(value.filter(Boolean))
16
+ const SECRET_PREFIX = "secret:"
17
+
18
+ function parseName(item: string): string {
19
+ return item.startsWith(SECRET_PREFIX) ? item.slice(SECRET_PREFIX.length) : item
20
+ }
21
+
22
+ function buildItem(name: string): string {
23
+ return `${SECRET_PREFIX}${name}`
24
+ }
25
+
26
+ export function SecretsInput({ id, value, onChange, disabled = false, ...rest }: SecretsInputProps) {
27
+ const normalize = (v: any): string[] =>
28
+ Array.isArray(v) ? v.filter(Boolean).map((item) => item.startsWith(SECRET_PREFIX) ? item : buildItem(item)) : []
29
+ const [items, setItems] = useState<string[]>(normalize(value))
18
30
  const [draft, setDraft] = useState("")
31
+ const mounted = useRef(false)
19
32
 
20
33
  useEffect(() => {
21
- const filtered = value.filter(Boolean)
34
+ const filtered = normalize(value)
35
+ if (!mounted.current) {
36
+ mounted.current = true
37
+ const raw = Array.isArray(value) ? value.filter(Boolean) : []
38
+ if (JSON.stringify(filtered) !== JSON.stringify(raw)) {
39
+ onChange?.(filtered)
40
+ }
41
+ return
42
+ }
22
43
  if (JSON.stringify(filtered) !== JSON.stringify(items)) {
23
44
  setItems(filtered)
24
45
  }
25
46
  }, [value])
26
47
 
27
48
  const handleAdd = () => {
28
- if (!draft || items.includes(draft)) return
29
- const next = [...items, draft]
49
+ if (!draft) return
50
+ const built = buildItem(draft)
51
+ if (items.includes(built)) return
52
+ const next = [...items, built]
30
53
  setItems(next)
31
54
  onChange?.(next)
32
55
  setDraft("")
@@ -41,19 +64,26 @@ export function SecretsInput({ id, value = [], onChange, disabled = false, ...re
41
64
  return (
42
65
  <div className="space-y-2">
43
66
  {items.map((item, index) => (
44
- <div key={index} className="flex items-center gap-2">
45
- <span className="flex-1 text-sm px-3 h-10 flex items-center border border-ibm-gray-20 bg-ibm-gray-10 text-ibm-gray-100 truncate font-mono">
46
- {item}
47
- </span>
48
- <Button
49
- type="button"
50
- variant="secondary"
51
- onClick={() => handleRemove(index)}
52
- disabled={disabled}
53
- className="rounded-none h-10 w-10 p-0 shrink-0"
54
- >
55
- <X className="h-3.5 w-3.5" />
56
- </Button>
67
+ <div
68
+ key={index}
69
+ className="flex items-center h-10 border border-ibm-gray-20 bg-white hover:border-ibm-gray-40 transition-colors"
70
+ >
71
+ <div className="flex items-center gap-1.5 px-3 flex-1 min-w-0">
72
+ <FileCode className="w-3 h-3 text-ibm-gray-50 shrink-0" />
73
+ <span className="text-xs font-mono text-ibm-gray-100 truncate">
74
+ {parseName(item)}
75
+ </span>
76
+ </div>
77
+
78
+ {!disabled && (
79
+ <button
80
+ type="button"
81
+ onClick={() => handleRemove(index)}
82
+ className="px-2 h-full flex items-center text-ibm-gray-40 hover:text-red-500 transition-colors shrink-0 border-l border-ibm-gray-20"
83
+ >
84
+ <X className="w-3.5 h-3.5" />
85
+ </button>
86
+ )}
57
87
  </div>
58
88
  ))}
59
89
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orsetra/shared-ui",
3
- "version": "1.5.10",
3
+ "version": "1.5.12",
4
4
  "description": "Shared UI components for Orsetra platform",
5
5
  "main": "./index.ts",
6
6
  "types": "./index.ts",
@@ -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
- }