11from __future__ import annotations
22
33from dataclasses import dataclass , field
4- from typing import TYPE_CHECKING
4+ from typing import TYPE_CHECKING , Any
55
6- from pydantic import BaseModel , Field
6+ from pydantic import BaseModel , Field , computed_field
77from typing_extensions import Self
88
99from infrahub .core .account import GlobalPermission
@@ -35,21 +35,26 @@ def _get_full_name_schema(node: MainSchemaTypes) -> str:
3535class MenuDict :
3636 data : dict [str , MenuItemDict ] = field (default_factory = dict )
3737
38+ def get_item_location (self , name : str ) -> list [str ]:
39+ location , _ = self ._find_child_item (name = name , children = self .data )
40+ return location
41+
3842 def find_item (self , name : str ) -> MenuItemDict | None :
39- return self ._find_child_item (name = name , children = self .data )
43+ _ , item = self ._find_child_item (name = name , children = self .data )
44+ return item
4045
4146 @classmethod
42- def _find_child_item (cls , name : str , children : dict [str , MenuItemDict ]) -> MenuItemDict | None :
47+ def _find_child_item (cls , name : str , children : dict [str , MenuItemDict ]) -> tuple [ list [ str ], MenuItemDict | None ] :
4348 if name in children .keys ():
44- return children [name ]
49+ return [], children [name ]
4550
4651 for child in children .values ():
4752 if not child .children :
4853 continue
49- found = cls ._find_child_item (name = name , children = child .children )
54+ position , found = cls ._find_child_item (name = name , children = child .children )
5055 if found :
51- return found
52- return None
56+ return [ str ( child . identifier )] + position , found
57+ return [], None
5358
5459 def to_rest (self ) -> Menu :
5560 data : dict [str , list [MenuItemList ]] = {}
@@ -62,10 +67,44 @@ def to_rest(self) -> Menu:
6267
6368 return Menu (sections = data )
6469
65- # @staticmethod
66- # def _sort_menu_items(items: dict[str, MenuItem]) -> dict[str, MenuItem]:
67- # sorted_dict = dict(sorted(items.items(), key=lambda x: (x[1].order_weight, x[0]), reverse=False))
68- # return sorted_dict
70+ @classmethod
71+ def from_definition_list (cls , definitions : list [MenuItemDefinition ]) -> Self :
72+ menu = cls ()
73+ for definition in definitions :
74+ menu .data [definition .full_name ] = MenuItemDict .from_definition (definition = definition )
75+ return menu
76+
77+ def get_all_identifiers (self ) -> set [str ]:
78+ return {identifier for item in self .data .values () for identifier in item .get_all_identifiers ()}
79+
80+ @classmethod
81+ async def from_db (cls , db : InfrahubDatabase , nodes : list [CoreMenuItem ]) -> Self :
82+ menu = cls ()
83+ menu_by_ids = {menu_node .get_id (): MenuItemDict .from_node (menu_node ) for menu_node in nodes }
84+
85+ async def add_children (menu_item : MenuItemDict , menu_node : CoreMenuItem ) -> MenuItemDict :
86+ children = await menu_node .children .get_peers (db = db , peer_type = CoreMenuItem )
87+ for child_id , child_node in children .items ():
88+ child_menu_item = menu_by_ids [child_id ]
89+ child = await add_children (child_menu_item , child_node )
90+ menu_item .children [str (child .identifier )] = child
91+ return menu_item
92+
93+ for menu_node in nodes :
94+ menu_item = menu_by_ids [menu_node .get_id ()]
95+ parent = await menu_node .parent .get_peer (db = db , peer_type = CoreMenuItem )
96+ if parent :
97+ continue
98+
99+ children = await menu_node .children .get_peers (db = db , peer_type = CoreMenuItem )
100+ for child_id , child_node in children .items ():
101+ child_menu_item = menu_by_ids [child_id ]
102+ child = await add_children (child_menu_item , child_node )
103+ menu_item .children [str (child .identifier )] = child
104+
105+ menu .data [str (menu_item .identifier )] = menu_item
106+
107+ return menu
69108
70109
71110@dataclass
@@ -74,7 +113,11 @@ class Menu:
74113
75114
76115class MenuItem (BaseModel ):
77- identifier : str = Field (..., description = "Unique identifier for this menu item" )
116+ _id : str | None = None
117+ namespace : str = Field (..., description = "Namespace of the menu item" )
118+ name : str = Field (..., description = "Name of the menu item" )
119+ description : str = Field (default = "" , description = "Description of the menu item" )
120+ protected : bool = Field (default = False , description = "Whether the menu item is protected" )
78121 label : str = Field (..., description = "Title of the menu item" )
79122 path : str = Field (default = "" , description = "URL endpoint if applicable" )
80123 icon : str = Field (default = "" , description = "The icon to show for the current view" )
@@ -83,10 +126,27 @@ class MenuItem(BaseModel):
83126 section : MenuSection = MenuSection .OBJECT
84127 permissions : list [str ] = Field (default_factory = list )
85128
129+ @computed_field
130+ def identifier (self ) -> str :
131+ return f"{ self .namespace } { self .name } "
132+
133+ def get_path (self ) -> str | None :
134+ if self .path :
135+ return self .path
136+
137+ if self .kind :
138+ return f"/objects/{ self .kind } "
139+
140+ return None
141+
86142 @classmethod
87143 def from_node (cls , obj : CoreMenuItem ) -> Self :
88144 return cls (
89- identifier = get_full_name (obj ),
145+ _id = obj .get_id (),
146+ name = obj .name .value ,
147+ namespace = obj .namespace .value ,
148+ protected = obj .protected .value ,
149+ description = obj .description .value or "" ,
90150 label = obj .label .value or "" ,
91151 icon = obj .icon .value or "" ,
92152 order_weight = obj .order_weight .value ,
@@ -96,10 +156,30 @@ def from_node(cls, obj: CoreMenuItem) -> Self:
96156 permissions = obj .required_permissions .value or [],
97157 )
98158
159+ async def to_node (self , db : InfrahubDatabase , parent : CoreMenuItem | None = None ) -> CoreMenuItem :
160+ obj = await Node .init (db = db , schema = CoreMenuItem )
161+ await obj .new (
162+ db = db ,
163+ namespace = self .namespace ,
164+ name = self .name ,
165+ label = self .label ,
166+ kind = self .kind ,
167+ path = self .get_path (),
168+ description = self .description or None ,
169+ icon = self .icon or None ,
170+ protected = self .protected ,
171+ section = self .section .value ,
172+ order_weight = self .order_weight ,
173+ parent = parent .id if parent else None ,
174+ required_permissions = self .permissions ,
175+ )
176+ return obj
177+
99178 @classmethod
100179 def from_schema (cls , model : NodeSchema | GenericSchema | ProfileSchema | TemplateSchema ) -> Self :
101180 return cls (
102- identifier = get_full_name (model ),
181+ name = model .name ,
182+ namespace = model .namespace ,
103183 label = model .label or model .kind ,
104184 path = f"/objects/{ model .kind } " ,
105185 icon = model .icon or "" ,
@@ -111,8 +191,14 @@ class MenuItemDict(MenuItem):
111191 hidden : bool = False
112192 children : dict [str , MenuItemDict ] = Field (default_factory = dict , description = "Child objects" )
113193
194+ def get_all_identifiers (self ) -> set [str ]:
195+ identifiers : set [str ] = {str (self .identifier )}
196+ for child in self .children .values ():
197+ identifiers .update (child .get_all_identifiers ())
198+ return identifiers
199+
114200 def to_list (self ) -> MenuItemList :
115- data = self .model_dump (exclude = {"children" })
201+ data = self .model_dump (exclude = {"children" , "id" })
116202 unsorted_children = [child .to_list () for child in self .children .values () if child .hidden is False ]
117203 data ["children" ] = sorted (unsorted_children , key = lambda d : d .order_weight )
118204 return MenuItemList (** data )
@@ -125,12 +211,42 @@ def get_global_permissions(self) -> list[GlobalPermission]:
125211 permissions .append (GlobalPermission .from_string (input = permission ))
126212 return permissions
127213
214+ def diff_attributes (self , other : Self ) -> dict [str , Any ]:
215+ other_attributes = other .model_dump (exclude = {"children" })
216+ self_attributes = self .model_dump (exclude = {"children" })
217+ return {
218+ key : value
219+ for key , value in other_attributes .items ()
220+ if value != self_attributes [key ] and key not in ["id" , "children" ]
221+ }
222+
223+ @classmethod
224+ def from_definition (cls , definition : MenuItemDefinition ) -> Self :
225+ menu_item = cls (
226+ name = definition .name ,
227+ namespace = definition .namespace ,
228+ label = definition .label ,
229+ path = definition .get_path () or "" ,
230+ icon = definition .icon ,
231+ kind = definition .kind ,
232+ protected = definition .protected ,
233+ section = definition .section ,
234+ permissions = definition .permissions ,
235+ order_weight = definition .order_weight ,
236+ )
237+
238+ for child in definition .children :
239+ menu_item .children [child .full_name ] = MenuItemDict .from_definition (definition = child )
240+
241+ return menu_item
242+
128243
129244class MenuItemList (MenuItem ):
130245 children : list [MenuItemList ] = Field (default_factory = list , description = "Child objects" )
131246
132247
133248class MenuItemDefinition (BaseModel ):
249+ _id : str | None = None
134250 namespace : str
135251 name : str
136252 label : str
@@ -162,6 +278,22 @@ async def to_node(self, db: InfrahubDatabase, parent: CoreMenuItem | None = None
162278 )
163279 return obj
164280
281+ @classmethod
282+ async def from_node (cls , node : CoreMenuItem ) -> Self :
283+ return cls (
284+ _id = node .get_id (),
285+ namespace = node .namespace .value ,
286+ name = node .name .value ,
287+ label = node .label .value or "" ,
288+ description = node .description .value or "" ,
289+ icon = node .icon .value or "" ,
290+ protected = node .protected .value ,
291+ path = node .path .value or "" ,
292+ kind = node .kind .value or "" ,
293+ section = node .section .value ,
294+ order_weight = node .order_weight .value ,
295+ )
296+
165297 def get_path (self ) -> str | None :
166298 if self .path :
167299 return self .path
0 commit comments