@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.
@@ -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}
@@ -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 overflow-x-auto -mx-4 sm:-mx-6 px-4 sm:px-6 min-h-10 ${
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"
@@ -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
- return path.startsWith(BASE_PATH) ? path.slice(BASE_PATH.length) : path
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 = [], onChange, disabled = false, ...rest }: ResourcesInputProps) {
36
- const [items, setItems] = useState<string[]>(value.filter(Boolean))
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.filter(Boolean)
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}${editingPathSuffix}`
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 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>
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 pr-2 min-w-0"
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 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"
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}</span>
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
  })}
@@ -32,7 +32,7 @@ export function SecretInput({
32
32
  </div>
33
33
  )
34
34
  }
35
-
35
+ console.log(type)
36
36
  const secrets = ctx.secretsMap.get(type || 'configs') || []
37
37
  return (
38
38
  <div className="flex items-center gap-2">
@@ -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.11",
3
+ "version": "1.5.13",
4
4
  "description": "Shared UI components for Orsetra platform",
5
5
  "main": "./index.ts",
6
6
  "types": "./index.ts",