@orsetra/shared-ui 1.4.0 → 1.5.0
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/layout-container.tsx +1 -1
- package/components/layout/page-with-side-panel.tsx +42 -1
- package/components/layout/sidebar/main-sidebar.tsx +38 -2
- package/components/layout/sidebar/sidebar.tsx +88 -39
- package/components/ui/secret-context.tsx +4 -7
- package/components/ui/secret-input.tsx +2 -25
- package/package.json +1 -1
|
@@ -122,7 +122,7 @@ function LayoutContent({
|
|
|
122
122
|
/>
|
|
123
123
|
|
|
124
124
|
<div className="flex-1 flex flex-col min-w-0">
|
|
125
|
-
<header className="h-14 bg-gray-50
|
|
125
|
+
<header className="h-14 bg-gray-50 flex-shrink-0">
|
|
126
126
|
<div className="h-full px-4 md:px-6 flex items-center justify-between">
|
|
127
127
|
{isHidden ? (
|
|
128
128
|
<Button
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import { ReactNode, useState } from "react"
|
|
4
|
-
import { X, ChevronLeft } from "lucide-react"
|
|
4
|
+
import { X, ChevronLeft, SlidersHorizontal } from "lucide-react"
|
|
5
5
|
import { Button } from "../ui/button"
|
|
6
6
|
import { cn } from "../../lib/utils"
|
|
7
7
|
|
|
@@ -20,6 +20,9 @@ interface PageWithSidePanelProps {
|
|
|
20
20
|
contentHeaderClassName?: string
|
|
21
21
|
sidePanelClassName?: string
|
|
22
22
|
sidePanelHeaderClassName?: string
|
|
23
|
+
/** Label for the mobile floating toggle button. When provided, a button
|
|
24
|
+
* appears on small screens (<lg) that opens the side panel as an overlay. */
|
|
25
|
+
mobileToggleLabel?: string
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
const PANEL_WIDTHS = {
|
|
@@ -43,8 +46,10 @@ export function PageWithSidePanel({
|
|
|
43
46
|
contentHeaderClassName,
|
|
44
47
|
sidePanelClassName,
|
|
45
48
|
sidePanelHeaderClassName,
|
|
49
|
+
mobileToggleLabel,
|
|
46
50
|
}: PageWithSidePanelProps) {
|
|
47
51
|
const [isOpen, setIsOpen] = useState(defaultOpen)
|
|
52
|
+
const [mobileOpen, setMobileOpen] = useState(false)
|
|
48
53
|
const panelWidthClass = PANEL_WIDTHS[sidePanelWidth]
|
|
49
54
|
|
|
50
55
|
const handleClose = () => {
|
|
@@ -131,8 +136,44 @@ export function PageWithSidePanel({
|
|
|
131
136
|
<ChevronLeft className="h-4 w-4 text-ibm-gray-70" />
|
|
132
137
|
</button>
|
|
133
138
|
)}
|
|
139
|
+
|
|
140
|
+
{/* Mobile overlay — only rendered below lg breakpoint */}
|
|
141
|
+
{mobileOpen && (
|
|
142
|
+
<div className="lg:hidden fixed inset-0 z-50 flex">
|
|
143
|
+
<div className="absolute inset-0 bg-black/40" onClick={() => setMobileOpen(false)} />
|
|
144
|
+
<div className="relative ml-auto flex flex-col bg-white shadow-xl h-full w-80 max-w-[90vw]">
|
|
145
|
+
<div className={cn(
|
|
146
|
+
"flex items-center justify-between h-14 flex-shrink-0 px-4 border-b border-ibm-gray-20",
|
|
147
|
+
sidePanelHeaderClassName
|
|
148
|
+
)}>
|
|
149
|
+
<div className="flex-1 min-w-0">{sidePanelHeader}</div>
|
|
150
|
+
<Button
|
|
151
|
+
variant="ghost"
|
|
152
|
+
size="xs"
|
|
153
|
+
onClick={() => setMobileOpen(false)}
|
|
154
|
+
className="h-8 w-8 p-0 ml-2 flex-shrink-0"
|
|
155
|
+
>
|
|
156
|
+
<X className="h-4 w-4" />
|
|
157
|
+
</Button>
|
|
158
|
+
</div>
|
|
159
|
+
<div className="flex-1 overflow-y-auto">{sidePanel}</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
134
163
|
</>
|
|
135
164
|
)}
|
|
165
|
+
|
|
166
|
+
{/* Mobile floating toggle button */}
|
|
167
|
+
{sidePanel && mobileToggleLabel && (
|
|
168
|
+
<button
|
|
169
|
+
onClick={() => setMobileOpen(true)}
|
|
170
|
+
className="lg:hidden fixed bottom-5 right-4 z-40 flex items-center gap-2 px-4 py-2.5 bg-ibm-blue-60 text-white text-sm font-medium shadow-lg hover:bg-ibm-blue-70 transition-colors"
|
|
171
|
+
aria-label={mobileToggleLabel}
|
|
172
|
+
>
|
|
173
|
+
<SlidersHorizontal className="h-4 w-4" />
|
|
174
|
+
{mobileToggleLabel}
|
|
175
|
+
</button>
|
|
176
|
+
)}
|
|
136
177
|
</div>
|
|
137
178
|
)
|
|
138
179
|
}
|
|
@@ -6,7 +6,7 @@ import { cn } from "../../../lib/utils"
|
|
|
6
6
|
import { Button } from "../../ui/button"
|
|
7
7
|
import { Logo } from "../../ui/logo"
|
|
8
8
|
import { type MainMenuItem, type SidebarMenus } from "./data"
|
|
9
|
-
import { X, Menu } from "lucide-react"
|
|
9
|
+
import { X, Menu, ChevronDown, ChevronRight } from "lucide-react"
|
|
10
10
|
|
|
11
11
|
export type SidebarMode = 'expanded' | 'minimized' | 'hidden'
|
|
12
12
|
|
|
@@ -36,12 +36,18 @@ export function MainSidebar({
|
|
|
36
36
|
const isMinimized = mode === 'minimized'
|
|
37
37
|
const [hoveredMenu, setHoveredMenu] = React.useState<string | null>(null)
|
|
38
38
|
const [flyoutPos, setFlyoutPos] = React.useState<{ top: number; left: number } | null>(null)
|
|
39
|
+
const [expandedMenu, setExpandedMenu] = React.useState<string | null>(null)
|
|
39
40
|
const closeTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
|
|
40
41
|
|
|
41
42
|
React.useEffect(() => {
|
|
42
43
|
return () => { if (closeTimeoutRef.current) clearTimeout(closeTimeoutRef.current) }
|
|
43
44
|
}, [])
|
|
44
45
|
|
|
46
|
+
// Reset expanded menu when sidebar closes
|
|
47
|
+
React.useEffect(() => {
|
|
48
|
+
if (!isOpen) setExpandedMenu(null)
|
|
49
|
+
}, [isOpen])
|
|
50
|
+
|
|
45
51
|
const handleFlyoutMouseEnter = (menuId: string, rect?: DOMRect) => {
|
|
46
52
|
if (closeTimeoutRef.current) clearTimeout(closeTimeoutRef.current)
|
|
47
53
|
if (rect) setFlyoutPos({ top: rect.top, left: rect.right + 8 })
|
|
@@ -53,6 +59,14 @@ export function MainSidebar({
|
|
|
53
59
|
}
|
|
54
60
|
|
|
55
61
|
const handleMenuClick = (menuId: string) => {
|
|
62
|
+
const hasSubMenu = (sidebarMenus[menuId]?.length ?? 0) > 0
|
|
63
|
+
|
|
64
|
+
// In mobile overlay mode (not minimized, overlay open): expand inline instead of navigating
|
|
65
|
+
if (!isMinimized && isOpen && hasSubMenu) {
|
|
66
|
+
setExpandedMenu((prev) => (prev === menuId ? null : menuId))
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
56
70
|
onMenuSelect(menuId)
|
|
57
71
|
|
|
58
72
|
if (!isMinimized && onSecondarySidebarOpen) {
|
|
@@ -153,8 +167,30 @@ export function MainSidebar({
|
|
|
153
167
|
"h-5 w-5 flex-shrink-0",
|
|
154
168
|
isActive ? "text-interactive" : "text-text-secondary"
|
|
155
169
|
)} />
|
|
156
|
-
{!isMinimized && <span className="text-base">{item.label}</span>}
|
|
170
|
+
{!isMinimized && <span className="text-base flex-1">{item.label}</span>}
|
|
171
|
+
{!isMinimized && hasSubMenu && isOpen && (
|
|
172
|
+
expandedMenu === item.id
|
|
173
|
+
? <ChevronDown className="h-4 w-4 text-text-secondary flex-shrink-0" />
|
|
174
|
+
: <ChevronRight className="h-4 w-4 text-text-secondary flex-shrink-0" />
|
|
175
|
+
)}
|
|
157
176
|
</button>
|
|
177
|
+
|
|
178
|
+
{/* Inline accordion sub-items — shown on mobile (overlay open, not minimized) */}
|
|
179
|
+
{!isMinimized && isOpen && expandedMenu === item.id && hasSubMenu && (
|
|
180
|
+
<div className="pl-4 pb-1">
|
|
181
|
+
{sidebarMenus[item.id].map((subItem) => (
|
|
182
|
+
<a
|
|
183
|
+
key={subItem.id}
|
|
184
|
+
href={buildSubItemHref(item.id, subItem.href)}
|
|
185
|
+
onClick={(e) => handleSubMenuClick(e, item.id, subItem.href)}
|
|
186
|
+
className="flex items-center gap-3 px-3 py-2 text-sm text-text-secondary hover:bg-ui-background hover:text-text-primary transition-colors no-underline border-l-2 border-ui-border hover:border-interactive"
|
|
187
|
+
>
|
|
188
|
+
<subItem.icon className="h-4 w-4 flex-shrink-0 text-text-secondary" />
|
|
189
|
+
{subItem.name}
|
|
190
|
+
</a>
|
|
191
|
+
))}
|
|
192
|
+
</div>
|
|
193
|
+
)}
|
|
158
194
|
</div>
|
|
159
195
|
)
|
|
160
196
|
})}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import * as React from "react"
|
|
4
4
|
import { Slot } from "@radix-ui/react-slot"
|
|
5
5
|
import { VariantProps, cva } from "class-variance-authority"
|
|
6
|
-
import { PanelLeft } from "lucide-react"
|
|
6
|
+
import { PanelLeft, ChevronLeft, ChevronRight } from "lucide-react"
|
|
7
7
|
import Link from "next/link"
|
|
8
8
|
import { usePathname, useSearchParams } from "next/navigation"
|
|
9
9
|
|
|
@@ -23,8 +23,7 @@ import { Logo } from "../../ui/logo"
|
|
|
23
23
|
import { type SidebarMenus, type SubMenuItem } from "./data"
|
|
24
24
|
import { Skeleton } from "../skeleton"
|
|
25
25
|
|
|
26
|
-
const
|
|
27
|
-
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
|
26
|
+
const SIDEBAR_STORAGE_KEY = "sidebar:state"
|
|
28
27
|
const SIDEBAR_WIDTH = "14rem"
|
|
29
28
|
const SIDEBAR_WIDTH_ICON = "3rem"
|
|
30
29
|
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
|
@@ -75,8 +74,19 @@ const SidebarProvider = React.forwardRef<
|
|
|
75
74
|
|
|
76
75
|
// This is the internal state of the sidebar.
|
|
77
76
|
// We use openProp and setOpenProp for control from outside the component.
|
|
77
|
+
// Initialize from defaultOpen (SSR-safe); sync from localStorage after mount.
|
|
78
78
|
const [_open, _setOpen] = React.useState(defaultOpen)
|
|
79
79
|
const open = openProp ?? _open
|
|
80
|
+
|
|
81
|
+
// Hydrate from localStorage on first client render (avoids SSR mismatch).
|
|
82
|
+
React.useEffect(() => {
|
|
83
|
+
try {
|
|
84
|
+
const stored = localStorage.getItem(SIDEBAR_STORAGE_KEY)
|
|
85
|
+
if (stored !== null) _setOpen(stored === "true")
|
|
86
|
+
} catch {}
|
|
87
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
88
|
+
}, [])
|
|
89
|
+
|
|
80
90
|
const setOpen = React.useCallback(
|
|
81
91
|
(value: boolean | ((value: boolean) => boolean)) => {
|
|
82
92
|
const openState = typeof value === "function" ? value(open) : value
|
|
@@ -86,8 +96,10 @@ const SidebarProvider = React.forwardRef<
|
|
|
86
96
|
_setOpen(openState)
|
|
87
97
|
}
|
|
88
98
|
|
|
89
|
-
//
|
|
90
|
-
|
|
99
|
+
// Persist state so it survives client-side navigation and page reloads.
|
|
100
|
+
try {
|
|
101
|
+
localStorage.setItem(SIDEBAR_STORAGE_KEY, String(openState))
|
|
102
|
+
} catch {}
|
|
91
103
|
},
|
|
92
104
|
[setOpenProp, open]
|
|
93
105
|
)
|
|
@@ -174,76 +186,113 @@ interface SidebarProps {
|
|
|
174
186
|
function Sidebar({ currentMenu, onMainMenuToggle, sidebarMenus = {}, main_base_url = "", sectionLabels = {}, getCurrentMenuItem }: SidebarProps = {}) {
|
|
175
187
|
const pathname = usePathname()
|
|
176
188
|
const searchParams = useSearchParams()
|
|
177
|
-
const { state } = useSidebar()
|
|
189
|
+
const { state, toggleSidebar } = useSidebar()
|
|
178
190
|
|
|
179
|
-
// Micro apps: utilise 'items' comme clé standard
|
|
180
|
-
// Main app: utilise currentMenu pour sélectionner le bon groupe
|
|
181
191
|
const currentNavigation: SubMenuItem[] = sidebarMenus['items']
|
|
182
192
|
|| (currentMenu && sidebarMenus[currentMenu])
|
|
183
193
|
|| []
|
|
184
194
|
|
|
195
|
+
const isCollapsed = state === "collapsed"
|
|
196
|
+
|
|
185
197
|
return (
|
|
186
|
-
<div
|
|
187
|
-
|
|
188
|
-
{
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
<
|
|
200
|
-
|
|
198
|
+
<div
|
|
199
|
+
className="h-screen sticky top-0 flex flex-col bg-gray-50 min-w-[var(--sidebar-width-icon)] transition-[width] duration-200 overflow-visible"
|
|
200
|
+
style={{ width: isCollapsed ? "var(--sidebar-width-icon)" : "var(--sidebar-width)" }}
|
|
201
|
+
>
|
|
202
|
+
{/* Header */}
|
|
203
|
+
<div className={cn(
|
|
204
|
+
"h-14 flex items-center flex-shrink-0",
|
|
205
|
+
isCollapsed ? "justify-center px-2" : "justify-between px-4"
|
|
206
|
+
)}>
|
|
207
|
+
{isCollapsed ? (
|
|
208
|
+
<Logo iconOnly />
|
|
209
|
+
) : (
|
|
210
|
+
<>
|
|
211
|
+
<Logo />
|
|
212
|
+
{onMainMenuToggle && (
|
|
213
|
+
<Button
|
|
214
|
+
variant="ghost"
|
|
215
|
+
size="sm"
|
|
216
|
+
onClick={onMainMenuToggle}
|
|
217
|
+
className="h-8 w-8 p-0 hover:bg-ui-background rounded-lg"
|
|
218
|
+
title="Ouvrir le menu principal"
|
|
219
|
+
>
|
|
220
|
+
<Menu className="h-4 w-4" />
|
|
221
|
+
</Button>
|
|
222
|
+
)}
|
|
223
|
+
</>
|
|
201
224
|
)}
|
|
202
225
|
</div>
|
|
203
226
|
|
|
204
|
-
{/*
|
|
205
|
-
{currentMenu && sectionLabels[currentMenu] && (
|
|
206
|
-
<div className="h-
|
|
207
|
-
<h2 className="text-
|
|
227
|
+
{/* Section label — hidden when collapsed */}
|
|
228
|
+
{!isCollapsed && currentMenu && sectionLabels[currentMenu] && (
|
|
229
|
+
<div className="h-10 flex items-center px-4 flex-shrink-0">
|
|
230
|
+
<h2 className="text-xs font-semibold text-text-secondary uppercase tracking-wide">
|
|
208
231
|
{sectionLabels[currentMenu]}
|
|
209
232
|
</h2>
|
|
210
233
|
</div>
|
|
211
234
|
)}
|
|
212
235
|
|
|
213
|
-
|
|
236
|
+
{/* Nav */}
|
|
237
|
+
<nav className={cn(
|
|
238
|
+
"flex-1 overflow-y-auto",
|
|
239
|
+
isCollapsed ? "px-1 py-3 space-y-0.5" : "px-3 py-4 space-y-1"
|
|
240
|
+
)}>
|
|
214
241
|
{currentNavigation.map((item) => {
|
|
215
|
-
// Use custom function if provided, otherwise use default pathname matching
|
|
216
242
|
const activeItemId = getCurrentMenuItem ? getCurrentMenuItem(pathname, searchParams) : null
|
|
217
|
-
const isActive = activeItemId
|
|
218
|
-
|
|
243
|
+
const isActive = activeItemId
|
|
244
|
+
? item.id === activeItemId
|
|
245
|
+
: (pathname === item.href || pathname.startsWith(`${item.href}/`))
|
|
246
|
+
|
|
219
247
|
const handleClick = (e: React.MouseEvent) => {
|
|
220
248
|
const href = item.href
|
|
221
249
|
if (href.startsWith('http://') || href.startsWith('https://')) {
|
|
222
250
|
e.preventDefault()
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
251
|
+
window.location.href = new URL(href).pathname
|
|
252
|
+
}
|
|
226
253
|
}
|
|
227
|
-
|
|
228
|
-
|
|
254
|
+
|
|
255
|
+
const linkEl = (
|
|
229
256
|
<Link
|
|
230
257
|
key={item.id}
|
|
231
258
|
href={item.href}
|
|
232
259
|
onClick={handleClick}
|
|
233
260
|
className={cn(
|
|
234
|
-
"flex items-center
|
|
261
|
+
"flex items-center transition-colors border-l-4",
|
|
262
|
+
isCollapsed ? "justify-center p-2" : "px-3 py-2 gap-x-3 text-sm",
|
|
235
263
|
isActive
|
|
236
264
|
? "bg-interactive/10 text-interactive font-medium border-interactive"
|
|
237
265
|
: "text-text-secondary hover:bg-ui-background hover:text-text-primary border-transparent"
|
|
238
266
|
)}
|
|
239
267
|
>
|
|
240
268
|
<item.icon className={cn("h-4 w-4 flex-shrink-0", isActive ? "text-interactive" : "text-text-secondary")} />
|
|
241
|
-
{item.name}
|
|
269
|
+
{!isCollapsed && item.name}
|
|
242
270
|
</Link>
|
|
243
271
|
)
|
|
272
|
+
|
|
273
|
+
if (isCollapsed) {
|
|
274
|
+
return (
|
|
275
|
+
<Tooltip key={item.id}>
|
|
276
|
+
<TooltipTrigger asChild>{linkEl}</TooltipTrigger>
|
|
277
|
+
<TooltipContent side="right" align="center">{item.name}</TooltipContent>
|
|
278
|
+
</Tooltip>
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
return linkEl
|
|
244
282
|
})}
|
|
245
283
|
</nav>
|
|
246
284
|
|
|
285
|
+
{/* Collapse / expand toggle */}
|
|
286
|
+
<div className={cn("flex-shrink-0 p-2", isCollapsed ? "flex justify-center" : "flex justify-end")}>
|
|
287
|
+
<button
|
|
288
|
+
type="button"
|
|
289
|
+
onClick={toggleSidebar}
|
|
290
|
+
className="h-8 w-8 flex items-center justify-center text-text-secondary hover:text-text-primary hover:bg-ui-background rounded-md transition-colors"
|
|
291
|
+
title={isCollapsed ? "Expand sidebar" : "Collapse sidebar"}
|
|
292
|
+
>
|
|
293
|
+
{isCollapsed ? <ChevronRight className="h-4 w-4" /> : <ChevronLeft className="h-4 w-4" />}
|
|
294
|
+
</button>
|
|
295
|
+
</div>
|
|
247
296
|
</div>
|
|
248
297
|
)
|
|
249
298
|
}
|
|
@@ -679,7 +728,7 @@ const SidebarMenuSub = React.forwardRef<
|
|
|
679
728
|
ref={ref}
|
|
680
729
|
data-sidebar="menu-sub"
|
|
681
730
|
className={cn(
|
|
682
|
-
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1
|
|
731
|
+
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 px-2.5 py-0.5",
|
|
683
732
|
"group-data-[collapsible=icon]:hidden",
|
|
684
733
|
className
|
|
685
734
|
)}
|
|
@@ -9,23 +9,20 @@ export interface SecretEntry {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
interface SecretContextValue {
|
|
12
|
-
|
|
13
|
-
fetchSecrets: (templateId?: string) => Promise<SecretEntry[]>
|
|
12
|
+
secretsMap: Map<string, SecretEntry[]>
|
|
14
13
|
}
|
|
15
14
|
|
|
16
15
|
const SecretContext = createContext<SecretContextValue | null>(null)
|
|
17
16
|
|
|
18
17
|
export function SecretContextProvider({
|
|
19
18
|
children,
|
|
20
|
-
|
|
21
|
-
fetchSecrets,
|
|
19
|
+
secretsMap,
|
|
22
20
|
}: {
|
|
23
21
|
children: ReactNode
|
|
24
|
-
|
|
25
|
-
fetchSecrets: (templateId?: string) => Promise<SecretEntry[]>
|
|
22
|
+
secretsMap: Map<string, SecretEntry[]>
|
|
26
23
|
}) {
|
|
27
24
|
return (
|
|
28
|
-
<SecretContext.Provider value={{
|
|
25
|
+
<SecretContext.Provider value={{ secretsMap }}>
|
|
29
26
|
{children}
|
|
30
27
|
</SecretContext.Provider>
|
|
31
28
|
)
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect } from "react"
|
|
4
|
-
import { Loader2 } from "lucide-react"
|
|
5
3
|
import { SelectInput } from "./select-input"
|
|
6
4
|
import { useSecretContext } from "./secret-context"
|
|
7
|
-
import type { SecretEntry } from "./secret-context"
|
|
8
5
|
|
|
9
6
|
interface SecretInputProps {
|
|
10
7
|
id?: string
|
|
@@ -26,26 +23,6 @@ export function SecretInput({
|
|
|
26
23
|
placeholder = "Select a secret",
|
|
27
24
|
}: SecretInputProps) {
|
|
28
25
|
const ctx = useSecretContext()
|
|
29
|
-
const [secrets, setSecrets] = useState<SecretEntry[]>([])
|
|
30
|
-
const [loading, setLoading] = useState(false)
|
|
31
|
-
|
|
32
|
-
useEffect(() => {
|
|
33
|
-
if (!ctx) return
|
|
34
|
-
setLoading(true)
|
|
35
|
-
ctx.fetchSecrets(type === "configs" ? ctx.templateId : type)
|
|
36
|
-
.then(setSecrets)
|
|
37
|
-
.catch(() => setSecrets([]))
|
|
38
|
-
.finally(() => setLoading(false))
|
|
39
|
-
}, [ctx?.templateId])
|
|
40
|
-
|
|
41
|
-
if (loading) {
|
|
42
|
-
return (
|
|
43
|
-
<div className="flex items-center gap-2 h-10 px-3 border border-ibm-gray-30 bg-white text-sm text-ibm-gray-60">
|
|
44
|
-
<Loader2 className="h-4 w-4 animate-spin flex-shrink-0" />
|
|
45
|
-
Loading secrets…
|
|
46
|
-
</div>
|
|
47
|
-
)
|
|
48
|
-
}
|
|
49
26
|
|
|
50
27
|
if (!ctx) {
|
|
51
28
|
return (
|
|
@@ -54,7 +31,7 @@ export function SecretInput({
|
|
|
54
31
|
</div>
|
|
55
32
|
)
|
|
56
33
|
}
|
|
57
|
-
|
|
34
|
+
const secrets = ctx.secretsMap.get(type || 'configs') || []
|
|
58
35
|
return (
|
|
59
36
|
<SelectInput
|
|
60
37
|
id={id}
|
|
@@ -63,7 +40,7 @@ export function SecretInput({
|
|
|
63
40
|
disabled={disabled}
|
|
64
41
|
className={className}
|
|
65
42
|
placeholder={placeholder}
|
|
66
|
-
options={secrets.map((s) => ({ label: s.description || s.name, value: s.name }))}
|
|
43
|
+
options={secrets.map((s) => ({ label: s.description || s.name, value: `secret:${s.name}` }))}
|
|
67
44
|
/>
|
|
68
45
|
)
|
|
69
46
|
}
|