@orsetra/shared-ui 1.5.11 → 1.5.13
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/sidebar/main-sidebar.tsx +56 -31
- package/components/layout/sidebar/sidebar.tsx +5 -4
- package/components/ui/detail-page-header.tsx +1 -1
- package/components/ui/index.ts +2 -0
- package/components/ui/resources-input.tsx +55 -43
- package/components/ui/secret-input.tsx +1 -1
- package/components/ui/secrets-input.tsx +50 -20
- package/package.json +1 -1
|
@@ -56,11 +56,12 @@ export function MainSidebar({
|
|
|
56
56
|
onSecondarySidebarOpen()
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
isMinimized ?
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
304
|
-
|
|
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}
|
|
@@ -102,7 +102,7 @@ export function DetailPageHeader({
|
|
|
102
102
|
|
|
103
103
|
{/* Tab bar — always rendered to keep header height consistent */}
|
|
104
104
|
<nav
|
|
105
|
-
className={`flex gap-0
|
|
105
|
+
className={`flex gap-0 px-4 sm:px-6 min-h-10 ${
|
|
106
106
|
tabBarPosition === "right" ? "justify-end" : "justify-start"
|
|
107
107
|
}`}
|
|
108
108
|
aria-label="Tabs"
|
package/components/ui/index.ts
CHANGED
|
@@ -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'
|
|
@@ -1,10 +1,9 @@
|
|
|
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 { Plus } from "lucide-react"
|
|
5
|
+
import { Plus, X, FileCode } from "lucide-react"
|
|
6
6
|
import { SecretInput } from "./secret-input"
|
|
7
|
-
import { FileChip } from "./file-chip"
|
|
8
7
|
|
|
9
8
|
interface ResourcesInputProps {
|
|
10
9
|
id?: string
|
|
@@ -29,18 +28,29 @@ function buildItem(name: string, path: string): string {
|
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
function pathSuffix(path: string): string {
|
|
32
|
-
|
|
31
|
+
const after = path.startsWith(BASE_PATH) ? path.slice(BASE_PATH.length) : path
|
|
32
|
+
return after.startsWith("/") ? after.slice(1) : after
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export function ResourcesInput({ id, value
|
|
36
|
-
const
|
|
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))
|
|
37
39
|
const [draft, setDraft] = useState("")
|
|
38
|
-
const [editingNameIndex, setEditingNameIndex] = useState<number | null>(null)
|
|
39
40
|
const [editingPathIndex, setEditingPathIndex] = useState<number | null>(null)
|
|
40
41
|
const [editingPathSuffix, setEditingPathSuffix] = useState("")
|
|
42
|
+
const mounted = useRef(false)
|
|
41
43
|
|
|
42
44
|
useEffect(() => {
|
|
43
|
-
const filtered = value
|
|
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
|
+
}
|
|
44
54
|
if (JSON.stringify(filtered) !== JSON.stringify(items)) {
|
|
45
55
|
setItems(filtered)
|
|
46
56
|
}
|
|
@@ -60,17 +70,6 @@ export function ResourcesInput({ id, value = [], onChange, disabled = false, ...
|
|
|
60
70
|
onChange?.(next)
|
|
61
71
|
}
|
|
62
72
|
|
|
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
73
|
const startPathEditing = (index: number, path: string) => {
|
|
75
74
|
if (disabled) return
|
|
76
75
|
setEditingPathIndex(index)
|
|
@@ -79,7 +78,7 @@ export function ResourcesInput({ id, value = [], onChange, disabled = false, ...
|
|
|
79
78
|
|
|
80
79
|
const confirmPathEdit = (index: number) => {
|
|
81
80
|
const { name } = parseItem(items[index])
|
|
82
|
-
const fullPath = `${BASE_PATH}
|
|
81
|
+
const fullPath = `${BASE_PATH}/${editingPathSuffix}`
|
|
83
82
|
const next = items.map((item, i) => i === index ? buildItem(name, fullPath) : item)
|
|
84
83
|
setItems(next)
|
|
85
84
|
onChange?.(next)
|
|
@@ -91,23 +90,25 @@ export function ResourcesInput({ id, value = [], onChange, disabled = false, ...
|
|
|
91
90
|
{items.map((item, index) => {
|
|
92
91
|
const { name, path } = parseItem(item)
|
|
93
92
|
const suffix = pathSuffix(path)
|
|
93
|
+
const isEditingPath = editingPathIndex === index
|
|
94
|
+
|
|
94
95
|
return (
|
|
95
|
-
<div
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
{
|
|
109
|
-
<div className="flex flex-1 items-center
|
|
110
|
-
<span className="
|
|
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>
|
|
111
112
|
<input
|
|
112
113
|
autoFocus
|
|
113
114
|
value={editingPathSuffix}
|
|
@@ -117,24 +118,35 @@ export function ResourcesInput({ id, value = [], onChange, disabled = false, ...
|
|
|
117
118
|
if (e.key === "Enter") confirmPathEdit(index)
|
|
118
119
|
if (e.key === "Escape") setEditingPathIndex(null)
|
|
119
120
|
}}
|
|
120
|
-
className="flex-1 text-sm font-mono text-ibm-gray-100 bg-transparent focus:outline-none
|
|
121
|
+
className="flex-1 text-sm font-mono text-ibm-gray-100 bg-transparent focus:outline-none min-w-0"
|
|
121
122
|
placeholder="/myapp"
|
|
122
123
|
/>
|
|
123
124
|
</div>
|
|
124
125
|
) : (
|
|
125
126
|
<span
|
|
126
127
|
onDoubleClick={() => startPathEditing(index, path)}
|
|
127
|
-
title={disabled ? undefined : "Double-click to edit"}
|
|
128
|
-
className={`flex flex-1 items-center
|
|
129
|
-
disabled ? "
|
|
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"
|
|
130
131
|
}`}
|
|
131
132
|
>
|
|
132
|
-
<span className="text-ibm-gray-40 shrink-0">{BASE_PATH}
|
|
133
|
-
<span className={suffix ? "text-ibm-gray-70" : "text-ibm-gray-40 italic"}>
|
|
134
|
-
{suffix || "
|
|
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 || "…"}
|
|
135
136
|
</span>
|
|
136
137
|
</span>
|
|
137
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
|
+
)}
|
|
138
150
|
</div>
|
|
139
151
|
)
|
|
140
152
|
})}
|
|
@@ -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
|
-
|
|
17
|
-
|
|
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
|
|
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
|
|
29
|
-
const
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|