@orsetra/shared-ui 1.1.0 → 1.1.2

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.
@@ -1,6 +1,6 @@
1
1
  // Layout components exports
2
2
  export { MainSidebar, type SidebarMode } from './sidebar/main-sidebar'
3
- export { menuItems, menuLabels, type MainMenuItem, type SubMenuItem, type SidebarMenus } from './sidebar/data'
3
+ export { resolveIcon, type MainMenuItem, type SubMenuItem, type SidebarMenus, type MenuApiResponse, type ApiMenuData, type ApiMainMenuItem, type ApiSubMenuItem, type ApiSubMenu } from './sidebar/data'
4
4
  export {
5
5
  Sidebar,
6
6
  SidebarContent,
@@ -4,7 +4,7 @@ import { useState, useEffect } from "react"
4
4
  import { usePathname } from "next/navigation"
5
5
  import { MainSidebar, type SidebarMode } from "./sidebar/main-sidebar"
6
6
  import { Sidebar, SidebarProvider, useSidebar } from "./sidebar/sidebar"
7
- import { type SidebarMenus } from "./sidebar/data"
7
+ import { type SidebarMenus, type MainMenuItem, type MenuApiResponse, resolveIcon } from "./sidebar/data"
8
8
  import { UserMenu, Button, type UserMenuConfig } from "../ui"
9
9
  import { getMenuFromPath } from "../../lib/menu-utils"
10
10
  import { useIsMobile } from "../../hooks/use-mobile"
@@ -13,14 +13,15 @@ import { Menu } from "lucide-react"
13
13
  interface LayoutContainerProps {
14
14
  main_base_url: string
15
15
  children: React.ReactNode
16
- sidebarMenus: SidebarMenus
16
+ sidebarMenus?: SidebarMenus
17
17
  user?: { profile?: { email?: string; preferred_username?: string } } | null
18
18
  onSignOut?: () => void
19
19
  mode?: SidebarMode
20
20
  userMenuConfig?: UserMenuConfig
21
+ fetchMenus?: () => Promise<MenuApiResponse>
21
22
  }
22
23
 
23
- function LayoutContent({ children, sidebarMenus, user, onSignOut, mode = 'expanded', userMenuConfig, main_base_url }: LayoutContainerProps) {
24
+ function LayoutContent({ children, sidebarMenus = {}, user, onSignOut, mode = 'expanded', userMenuConfig, main_base_url, fetchMenus }: LayoutContainerProps) {
24
25
  const pathname = usePathname()
25
26
  const { setOpen } = useSidebar()
26
27
  const isMobile = useIsMobile()
@@ -30,6 +31,45 @@ function LayoutContent({ children, sidebarMenus, user, onSignOut, mode = 'expand
30
31
  // Force hidden mode on mobile
31
32
  const isHidden = mode === 'hidden' || isMobile
32
33
 
34
+ const [mainMenuItems, setMainMenuItems] = useState<MainMenuItem[]>([])
35
+ const [fetchedSidebarMenus, setFetchedSidebarMenus] = useState<SidebarMenus>({})
36
+ const [sectionLabels, setSectionLabels] = useState<Record<string, string>>({})
37
+
38
+ // Load menus from API when fetchMenus is provided
39
+ useEffect(() => {
40
+ if (!fetchMenus) return
41
+ fetchMenus().then((response) => {
42
+ if (!response.success) return
43
+ const { main, subMenus } = response.data
44
+
45
+ setMainMenuItems(
46
+ main.map((item) => ({
47
+ id: item.id,
48
+ label: item.label,
49
+ icon: resolveIcon(item.icon),
50
+ }))
51
+ )
52
+
53
+ const menus: SidebarMenus = {}
54
+ const labels: Record<string, string> = {}
55
+ Object.entries(subMenus).forEach(([key, subMenu]) => {
56
+ menus[key] = subMenu.items.map((item) => ({
57
+ id: item.id,
58
+ name: item.label,
59
+ href: item.href,
60
+ icon: resolveIcon(item.icon),
61
+ }))
62
+ labels[key] = subMenu.header
63
+ })
64
+ setFetchedSidebarMenus(menus)
65
+ setSectionLabels(labels)
66
+ })
67
+ // eslint-disable-next-line react-hooks/exhaustive-deps
68
+ }, [])
69
+
70
+ // Effective sidebarMenus: fetched provides section keys, prop provides items key (micro apps)
71
+ const effectiveSidebarMenus: SidebarMenus = { ...fetchedSidebarMenus, ...sidebarMenus }
72
+
33
73
  // Close sidebar on route change (mobile)
34
74
  useEffect(() => {
35
75
  if (isMobile) {
@@ -54,9 +94,6 @@ function LayoutContent({ children, sidebarMenus, user, onSignOut, mode = 'expand
54
94
  setOpen(true)
55
95
  }
56
96
 
57
- const module = pathname.split('/')[1] || 'overview'
58
- const sidebarItems = sidebarMenus[module] || []
59
-
60
97
  return (
61
98
  <div className="flex h-screen w-full bg-white">
62
99
  {/* Desktop sidebar - hidden on mobile (isHidden is true on mobile) */}
@@ -64,13 +101,14 @@ function LayoutContent({ children, sidebarMenus, user, onSignOut, mode = 'expand
64
101
  <Sidebar
65
102
  currentMenu={currentMenu}
66
103
  onMainMenuToggle={handleMainSidebarToggle}
67
- sidebarMenus={sidebarMenus}
104
+ sidebarMenus={effectiveSidebarMenus}
68
105
  main_base_url={main_base_url}
106
+ sectionLabels={sectionLabels}
69
107
  />
70
108
  )}
71
-
109
+
72
110
  {/* MainSidebar - always available, opens as overlay when isHidden */}
73
- <MainSidebar
111
+ <MainSidebar
74
112
  main_base_url={main_base_url}
75
113
  isOpen={isMainSidebarOpen}
76
114
  onToggle={handleMainSidebarToggle}
@@ -78,9 +116,10 @@ function LayoutContent({ children, sidebarMenus, user, onSignOut, mode = 'expand
78
116
  currentMenu={currentMenu}
79
117
  onSecondarySidebarOpen={handleSecondarySidebarOpen}
80
118
  mode={isHidden ? "expanded" : mode}
81
- sidebarMenus={sidebarMenus}
119
+ sidebarMenus={effectiveSidebarMenus}
120
+ mainMenuItems={mainMenuItems}
82
121
  />
83
-
122
+
84
123
  <div className="flex-1 flex flex-col min-w-0">
85
124
  <header className="h-14 bg-gray-50 border-b border-ui-border flex-shrink-0">
86
125
  <div className="h-full px-4 md:px-6 flex items-center justify-between">
@@ -98,14 +137,14 @@ function LayoutContent({ children, sidebarMenus, user, onSignOut, mode = 'expand
98
137
  ) : (
99
138
  <div />
100
139
  )}
101
- <UserMenu
102
- username={user?.profile?.email || user?.profile?.preferred_username}
140
+ <UserMenu
141
+ username={user?.profile?.email || user?.profile?.preferred_username}
103
142
  onSignOut={onSignOut || (() => {})}
104
143
  menuConfig={userMenuConfig}
105
144
  />
106
145
  </div>
107
146
  </header>
108
-
147
+
109
148
  <main className="flex-1 overflow-auto">
110
149
  {children}
111
150
  </main>
@@ -62,7 +62,7 @@ export function PageWithSidePanel({
62
62
  )}
63
63
 
64
64
  {/* Content body */}
65
- <div className="px-6 py-8">
65
+ <div className="px-2 py-2">
66
66
  {children}
67
67
  </div>
68
68
  </div>
@@ -1,5 +1,3 @@
1
- "use client"
2
-
3
1
  import { type LucideIcon } from "lucide-react"
4
2
  import {
5
3
  Shield,
@@ -7,13 +5,30 @@ import {
7
5
  Settings,
8
6
  Rocket,
9
7
  LineChart,
8
+ Library,
9
+ Component,
10
+ FolderGit2,
11
+ Network,
12
+ Key,
13
+ Globe,
14
+ Activity,
15
+ Database,
16
+ Users,
17
+ Plug,
18
+ Box,
19
+ Monitor,
20
+ Server,
21
+ Lock,
22
+ Home,
23
+ FileText,
24
+ Code,
25
+ Cloud,
10
26
  } from "lucide-react"
11
27
 
12
- // Types pour les menus
13
28
  export interface MainMenuItem {
14
29
  id: string
15
30
  label: string
16
- icon: React.ReactNode
31
+ icon: LucideIcon
17
32
  }
18
33
 
19
34
  export interface SubMenuItem {
@@ -27,42 +42,62 @@ export interface SidebarMenus {
27
42
  [key: string]: SubMenuItem[]
28
43
  }
29
44
 
45
+ // API response types
46
+ export interface ApiMainMenuItem {
47
+ id: string
48
+ label: string
49
+ icon: string
50
+ }
30
51
 
31
- export const menuItems: MainMenuItem[] = [
32
- {
33
- id: "assets",
34
- label: "Assets",
35
- icon: <Rocket className="h-6 w-6" />
36
- },
37
- {
38
- id: "runtime",
39
- label: "Runtime",
40
- icon: <Package className="h-6 w-6" />
41
- },
42
- {
43
- id: "monitoring",
44
- label: "Monitoring",
45
- icon: <LineChart className="h-6 w-6" />
46
- },
47
- {
48
- id: "api-manager",
49
- label: "API Manager",
50
- icon: <Settings className="h-6 w-6" />
51
- },
52
- {
53
- id: "access",
54
- label: "Access Manager",
55
- icon: <Shield className="h-6 w-6" />
56
- },
52
+ export interface ApiSubMenuItem {
53
+ id: string
54
+ label: string
55
+ href: string
56
+ icon: string
57
+ }
57
58
 
58
- ]
59
+ export interface ApiSubMenu {
60
+ id: string
61
+ header: string
62
+ items: ApiSubMenuItem[]
63
+ }
59
64
 
60
- // Labels des menus principaux pour l'affichage
61
- export const menuLabels: Record<string, string> = {
62
- "assets": "Assets",
63
- "runtime": "Runtime",
64
- "monitoring": "Monitoring",
65
- "api-manager": "API Manager",
66
- "access": "Access Manager",
65
+ export interface ApiMenuData {
66
+ main: ApiMainMenuItem[]
67
+ subMenus: Record<string, ApiSubMenu>
67
68
  }
68
69
 
70
+ export interface MenuApiResponse {
71
+ success: boolean
72
+ data: ApiMenuData
73
+ }
74
+
75
+ const ICON_MAP: Record<string, LucideIcon> = {
76
+ Shield,
77
+ Package,
78
+ Settings,
79
+ Rocket,
80
+ LineChart,
81
+ Library,
82
+ Component,
83
+ FolderGit2,
84
+ Network,
85
+ Key,
86
+ Globe,
87
+ Activity,
88
+ Database,
89
+ Users,
90
+ Plug,
91
+ Box,
92
+ Monitor,
93
+ Server,
94
+ Lock,
95
+ Home,
96
+ FileText,
97
+ Code,
98
+ Cloud,
99
+ }
100
+
101
+ export function resolveIcon(name: string): LucideIcon {
102
+ return ICON_MAP[name] || Box
103
+ }
@@ -3,12 +3,10 @@
3
3
  import * as React from "react"
4
4
  import { cn } from "../../../lib/utils"
5
5
 
6
- import { useRouter } from "next/navigation"
7
6
  import { Button } from "../../ui/button"
8
7
  import { Logo } from "../../ui/logo"
9
- import { menuItems, type SidebarMenus } from "./data"
8
+ import { type MainMenuItem, type SidebarMenus } from "./data"
10
9
  import { X, Menu } from "lucide-react"
11
- import Link from "next/link"
12
10
 
13
11
  export type SidebarMode = 'expanded' | 'minimized' | 'hidden'
14
12
 
@@ -21,57 +19,55 @@ interface MainSidebarProps {
21
19
  mode?: SidebarMode
22
20
  onSecondarySidebarOpen?: () => void
23
21
  sidebarMenus?: SidebarMenus
22
+ mainMenuItems?: MainMenuItem[]
24
23
  }
25
24
 
26
- export function MainSidebar({
25
+ export function MainSidebar({
27
26
  main_base_url,
28
- isOpen,
29
- onToggle,
30
- onMenuSelect,
27
+ isOpen,
28
+ onToggle,
29
+ onMenuSelect,
31
30
  currentMenu,
32
31
  mode = 'expanded',
33
32
  onSecondarySidebarOpen,
34
- sidebarMenus = {}
33
+ sidebarMenus = {},
34
+ mainMenuItems = [],
35
35
  }: MainSidebarProps) {
36
36
  const isMinimized = mode === 'minimized'
37
- const router = useRouter()
38
37
  const [hoveredMenu, setHoveredMenu] = React.useState<string | null>(null)
39
38
 
40
39
  const handleMenuClick = (menuId: string) => {
41
40
  onMenuSelect(menuId)
42
-
41
+
43
42
  if (!isMinimized && onSecondarySidebarOpen) {
44
43
  onSecondarySidebarOpen()
45
44
  }
46
-
45
+
47
46
  const subMenus = sidebarMenus[menuId]
48
47
  if (subMenus && subMenus.length > 0) {
49
48
  const targetUrl = subMenus[0].href
50
- const path = targetUrl.startsWith('http://') || targetUrl.startsWith('https://')
51
- ? new URL(targetUrl).pathname
49
+ const path = targetUrl.startsWith('http://') || targetUrl.startsWith('https://')
50
+ ? new URL(targetUrl).pathname
52
51
  : targetUrl
53
52
  window.location.href = path
54
53
  } else {
55
54
  const targetUrl = `${main_base_url}/${menuId}`
56
- const path = targetUrl.startsWith('http://') || targetUrl.startsWith('https://')
57
- ? new URL(targetUrl).pathname
55
+ const path = targetUrl.startsWith('http://') || targetUrl.startsWith('https://')
56
+ ? new URL(targetUrl).pathname
58
57
  : targetUrl
59
58
  window.location.href = path
60
59
  }
61
-
60
+
62
61
  if (!isMinimized) {
63
- onToggle()
62
+ onToggle()
64
63
  }
65
64
  }
66
65
 
67
66
  const handleSubMenuClick = (e: React.MouseEvent, href: string) => {
68
67
  e.preventDefault()
69
- console.log('handleSubMenuClick - href:', href)
70
- const path = href.startsWith('http://') || href.startsWith('https://')
71
- ? new URL(href).pathname
68
+ const path = href.startsWith('http://') || href.startsWith('https://')
69
+ ? new URL(href).pathname
72
70
  : href
73
- console.log('handleSubMenuClick - path:', path)
74
- console.log('Redirecting to:', path)
75
71
  window.location.href = path
76
72
  setHoveredMenu(null)
77
73
  }
@@ -79,7 +75,7 @@ export function MainSidebar({
79
75
  return (
80
76
  <>
81
77
  {isOpen && !isMinimized && (
82
- <div
78
+ <div
83
79
  className="fixed inset-0 bg-black/50 z-40"
84
80
  onClick={onToggle}
85
81
  />
@@ -88,8 +84,8 @@ export function MainSidebar({
88
84
  <div className={cn(
89
85
  "fixed left-0 top-0 h-full bg-white border-r border-ui-border z-50 transform transition-all duration-300 ease-in-out",
90
86
  isMinimized ? "w-16" : "w-64 shadow-xl",
91
- isMinimized
92
- ? "translate-x-0"
87
+ isMinimized
88
+ ? "translate-x-0"
93
89
  : (isOpen ? "translate-x-0" : "-translate-x-full")
94
90
  )}>
95
91
  <div className={cn(
@@ -116,59 +112,58 @@ export function MainSidebar({
116
112
  isMinimized ? "p-2" : "p-3"
117
113
  )}>
118
114
  <nav className="space-y-2">
119
- {menuItems.map((item) => (
120
- <div
121
- key={item.id}
122
- className="relative"
123
- onMouseEnter={() => isMinimized && setHoveredMenu(item.id)}
124
- onMouseLeave={() => isMinimized && setHoveredMenu(null)}
125
- >
126
- <button
127
- onClick={() => handleMenuClick(item.id)}
128
- className={cn(
129
- "w-full flex items-center text-left rounded-lg transition-all duration-200 font-medium",
130
- isMinimized
131
- ? "justify-center p-3"
132
- : "gap-3 px-3 py-3",
133
- currentMenu === item.id
134
- ? "bg-interactive/10 text-interactive"
135
- : "text-text-primary hover:bg-ui-background hover:text-text-primary"
136
- )}
137
- title={isMinimized ? item.label : undefined}
115
+ {mainMenuItems.map((item) => {
116
+ const Icon = item.icon
117
+ return (
118
+ <div
119
+ key={item.id}
120
+ className="relative"
121
+ onMouseEnter={() => isMinimized && setHoveredMenu(item.id)}
122
+ onMouseLeave={() => isMinimized && setHoveredMenu(null)}
138
123
  >
139
- <div className={cn(
140
- "flex items-center justify-center",
141
- currentMenu === item.id
142
- ? "text-interactive"
143
- : "text-text-secondary"
144
- )}>
145
- {item.icon}
146
- </div>
147
- {!isMinimized && <span className="text-base">{item.label}</span>}
148
- </button>
124
+ <button
125
+ onClick={() => handleMenuClick(item.id)}
126
+ className={cn(
127
+ "w-full flex items-center text-left rounded-lg transition-all duration-200 font-medium",
128
+ isMinimized
129
+ ? "justify-center p-3"
130
+ : "gap-3 px-3 py-3",
131
+ currentMenu === item.id
132
+ ? "bg-interactive/10 text-interactive"
133
+ : "text-text-primary hover:bg-ui-background hover:text-text-primary"
134
+ )}
135
+ title={isMinimized ? item.label : undefined}
136
+ >
137
+ <Icon className={cn(
138
+ "h-6 w-6",
139
+ currentMenu === item.id ? "text-interactive" : "text-text-secondary"
140
+ )} />
141
+ {!isMinimized && <span className="text-base">{item.label}</span>}
142
+ </button>
149
143
 
150
- {isMinimized && hoveredMenu === item.id && sidebarMenus[item.id] && sidebarMenus[item.id].length > 0 && (
151
- <div className="absolute left-full top-0 ml-2 bg-white border border-ui-border rounded-lg shadow-xl z-50 min-w-[200px] py-2">
152
- <div className="px-4 py-2 border-b border-ui-border">
153
- <h3 className="text-sm font-semibold text-text-primary">{item.label}</h3>
144
+ {isMinimized && hoveredMenu === item.id && sidebarMenus[item.id] && sidebarMenus[item.id].length > 0 && (
145
+ <div className="absolute left-full top-0 ml-2 bg-white border border-ui-border rounded-lg shadow-xl z-50 min-w-[200px] py-2">
146
+ <div className="px-4 py-2 border-b border-ui-border">
147
+ <h3 className="text-sm font-semibold text-text-primary">{item.label}</h3>
148
+ </div>
149
+ <div className="py-1">
150
+ {sidebarMenus[item.id].map((subItem) => (
151
+ <a
152
+ key={subItem.id}
153
+ href={subItem.href}
154
+ onClick={(e) => handleSubMenuClick(e, subItem.href)}
155
+ className="flex items-center gap-3 px-4 py-2 text-sm text-text-secondary hover:bg-ui-background hover:text-text-primary transition-colors no-underline"
156
+ >
157
+ <subItem.icon className="h-4 w-4" />
158
+ {subItem.name}
159
+ </a>
160
+ ))}
161
+ </div>
154
162
  </div>
155
- <div className="py-1">
156
- {sidebarMenus[item.id].map((subItem) => (
157
- <a
158
- key={subItem.id}
159
- href={subItem.href}
160
- onClick={(e) => handleSubMenuClick(e, subItem.href)}
161
- className="flex items-center gap-3 px-4 py-2 text-sm text-text-secondary hover:bg-ui-background hover:text-text-primary transition-colors no-underline"
162
- >
163
- <subItem.icon className="h-4 w-4" />
164
- {subItem.name}
165
- </a>
166
- ))}
167
- </div>
168
- </div>
169
- )}
170
- </div>
171
- ))}
163
+ )}
164
+ </div>
165
+ )
166
+ })}
172
167
  </nav>
173
168
  </div>
174
169
  </div>
@@ -188,4 +183,4 @@ export function MainSidebarToggle({ onClick }: { onClick: () => void }) {
188
183
  <Menu className="h-4 w-4" />
189
184
  </Button>
190
185
  )
191
- }
186
+ }
@@ -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 { LineChart, PanelLeft, Plus } from "lucide-react"
6
+ import { PanelLeft } from "lucide-react"
7
7
  import Link from "next/link"
8
8
  import { usePathname } from "next/navigation"
9
9
 
@@ -19,17 +19,14 @@ import {
19
19
  TooltipTrigger,
20
20
  } from "../../ui/tooltip"
21
21
  import {
22
- Shield,
23
22
  Settings,
24
- Package,
25
- Rocket,
26
23
  Menu,
27
24
  Building2,
28
25
  User,
29
26
  ChevronUp,
30
27
  } from "lucide-react"
31
28
  import { Logo } from "../../ui/logo"
32
- import { menuLabels, type SidebarMenus, type SubMenuItem } from "./data"
29
+ import { type SidebarMenus, type SubMenuItem } from "./data"
33
30
  import { Skeleton } from "../skeleton"
34
31
 
35
32
  const SIDEBAR_COOKIE_NAME = "sidebar:state"
@@ -174,11 +171,12 @@ interface SidebarProps {
174
171
  onMainMenuToggle?: () => void
175
172
  sidebarMenus?: SidebarMenus
176
173
  main_base_url?: string
174
+ sectionLabels?: Record<string, string>
177
175
  }
178
176
 
179
177
 
180
178
 
181
- function Sidebar({ currentMenu, onMainMenuToggle, sidebarMenus = {}, main_base_url = "" }: SidebarProps = {}) {
179
+ function Sidebar({ currentMenu, onMainMenuToggle, sidebarMenus = {}, main_base_url = "", sectionLabels = {} }: SidebarProps = {}) {
182
180
  const pathname = usePathname()
183
181
  const { state } = useSidebar()
184
182
  const [settingsOpen, setSettingsOpen] = React.useState(false)
@@ -223,18 +221,11 @@ function Sidebar({ currentMenu, onMainMenuToggle, sidebarMenus = {}, main_base_u
223
221
  </div>
224
222
 
225
223
  {/* En-tête du menu principal sélectionné */}
226
- {currentMenu && menuLabels[currentMenu] && (
224
+ {currentMenu && sectionLabels[currentMenu] && (
227
225
  <div className="px-4 py-3 border-b border-ui-border">
228
- <div className="flex items-center gap-2">
229
- {currentMenu === "assets" && <Rocket className="h-4 w-4 text-interactive" />}
230
- {currentMenu === "runtime" && <Package className="h-4 w-4 text-interactive" />}
231
- {currentMenu === "api-manager" && <Settings className="h-4 w-4 text-interactive" />}
232
- {currentMenu === "access-manager" && <Shield className="h-4 w-4 text-interactive" />}
233
- {currentMenu === "monitoring" && <LineChart className="h-4 w-4 text-interactive" />}
234
- <h2 className="text-sm font-semibold text-text-primary">
235
- {menuLabels[currentMenu]}
236
- </h2>
237
- </div>
226
+ <h2 className="text-sm font-semibold text-text-primary">
227
+ {sectionLabels[currentMenu]}
228
+ </h2>
238
229
  </div>
239
230
  )}
240
231
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orsetra/shared-ui",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Shared UI components for Orsetra platform",
5
5
  "main": "./index.ts",
6
6
  "types": "./index.ts",