diff --git a/backend/infrahub/api/menu.py b/backend/infrahub/api/menu.py
index e622c61114..705d01ed62 100644
--- a/backend/infrahub/api/menu.py
+++ b/backend/infrahub/api/menu.py
@@ -3,21 +3,17 @@
from typing import TYPE_CHECKING
from fastapi import APIRouter, Depends
-from pydantic import BaseModel, Field
from infrahub.api.dependencies import get_branch_dep, get_current_user, get_db
from infrahub.core import registry
from infrahub.core.branch import Branch # noqa: TCH001
-from infrahub.core.constants import InfrahubKind
from infrahub.core.protocols import CoreMenuItem
-from infrahub.core.schema import NodeSchema
from infrahub.log import get_logger
from infrahub.menu.generator import generate_menu
from infrahub.menu.models import Menu # noqa: TCH001
if TYPE_CHECKING:
from infrahub.auth import AccountSession
- from infrahub.core.schema import MainSchemaTypes
from infrahub.database import InfrahubDatabase
@@ -25,229 +21,17 @@
router = APIRouter(prefix="/menu")
-class InterfaceMenu(BaseModel):
- title: str = Field(..., description="Title of the menu item")
- path: str = Field(default="", description="URL endpoint if applicable")
- icon: str = Field(default="", description="The icon to show for the current view")
- children: list[InterfaceMenu] = Field(default_factory=list, description="Child objects")
- kind: str = Field(default="")
-
- def __lt__(self, other: object) -> bool:
- if not isinstance(other, InterfaceMenu):
- raise NotImplementedError
- return self.title < other.title
-
- def list_title(self) -> str:
- return f"All {self.title}(s)"
-
-
-def add_to_menu(structure: dict[str, list[InterfaceMenu]], menu_item: InterfaceMenu) -> None:
- all_items = InterfaceMenu(title=menu_item.list_title(), path=menu_item.path, icon=menu_item.icon)
- menu_item.path = ""
- menu_item.icon = ""
- for child in structure[menu_item.kind]:
- menu_item.children.append(child)
- if child.kind in structure:
- add_to_menu(structure, child)
- menu_item.children.sort()
- menu_item.children.insert(0, all_items)
-
-
-def _extract_node_icon(model: MainSchemaTypes) -> str:
- if not model.icon:
- return ""
- return model.icon
-
-
@router.get("")
-async def get_menu(branch: Branch = Depends(get_branch_dep)) -> list[InterfaceMenu]:
- log.info("menu_request", branch=branch.name)
-
- full_schema = registry.schema.get_full(branch=branch, duplicate=False)
- objects = InterfaceMenu(title="Objects", children=[])
-
- structure: dict[str, list[InterfaceMenu]] = {}
-
- ipam = InterfaceMenu(
- title="IPAM",
- children=[
- InterfaceMenu(
- title="Namespaces",
- path=f"/objects/{InfrahubKind.IPNAMESPACE}",
- icon=_extract_node_icon(full_schema[InfrahubKind.IPNAMESPACE]),
- ),
- InterfaceMenu(
- title="IP Prefixes", path="/ipam/prefixes", icon=_extract_node_icon(full_schema[InfrahubKind.IPPREFIX])
- ),
- InterfaceMenu(
- title="IP Addresses",
- path="/ipam/addresses?ipam-tab=ip-details",
- icon=_extract_node_icon(full_schema[InfrahubKind.IPADDRESS]),
- ),
- ],
- )
-
- has_ipam = False
-
- for key in full_schema.keys():
- model = full_schema[key]
-
- if isinstance(model, NodeSchema) and (
- InfrahubKind.IPADDRESS in model.inherit_from or InfrahubKind.IPPREFIX in model.inherit_from
- ):
- has_ipam = True
-
- if not model.include_in_menu:
- continue
-
- menu_name = model.menu_placement or "base"
- if menu_name not in structure:
- structure[menu_name] = []
-
- structure[menu_name].append(
- InterfaceMenu(title=model.menu_title, path=f"/objects/{model.kind}", icon=model.icon or "", kind=model.kind)
- )
-
- for menu_item in structure["base"]:
- if menu_item.kind in structure:
- add_to_menu(structure, menu_item)
-
- objects.children.append(menu_item)
-
- objects.children.sort()
- groups = InterfaceMenu(
- title="Object Management",
- children=[
- InterfaceMenu(
- title="All Groups",
- path=f"/objects/{InfrahubKind.GENERICGROUP}",
- icon=_extract_node_icon(full_schema[InfrahubKind.GENERICGROUP]),
- ),
- InterfaceMenu(
- title="All Profiles",
- path=f"/objects/{InfrahubKind.PROFILE}",
- icon=_extract_node_icon(full_schema[InfrahubKind.PROFILE]),
- ),
- InterfaceMenu(
- title="Resource Manager",
- path="/resource-manager",
- icon=_extract_node_icon(full_schema[InfrahubKind.RESOURCEPOOL]),
- ),
- ],
- )
-
- unified_storage = InterfaceMenu(
- title="Unified Storage",
- children=[
- InterfaceMenu(title="Schema", path="/schema", icon="mdi:file-code"),
- InterfaceMenu(
- title="Repository",
- path=f"/objects/{InfrahubKind.GENERICREPOSITORY}",
- icon=_extract_node_icon(full_schema[InfrahubKind.GENERICREPOSITORY]),
- ),
- InterfaceMenu(
- title="GraphQL Query",
- path=f"/objects/{InfrahubKind.GRAPHQLQUERY}",
- icon=_extract_node_icon(full_schema[InfrahubKind.GRAPHQLQUERY]),
- ),
- ],
- )
- change_control = InterfaceMenu(
- title="Change Control",
- children=[
- InterfaceMenu(title="Branches", path="/branches", icon="mdi:layers-triple"),
- InterfaceMenu(
- title="Proposed Changes",
- path="/proposed-changes",
- icon=_extract_node_icon(full_schema[InfrahubKind.PROPOSEDCHANGE]),
- ),
- InterfaceMenu(
- title="Check Definition",
- path=f"/objects/{InfrahubKind.CHECKDEFINITION}",
- icon=_extract_node_icon(full_schema[InfrahubKind.CHECKDEFINITION]),
- ),
- InterfaceMenu(title="Tasks", path="/tasks", icon="mdi:shield-check"),
- ],
- )
- deployment = InterfaceMenu(
- title="Deployment",
- children=[
- InterfaceMenu(
- title="Artifact",
- path=f"/objects/{InfrahubKind.ARTIFACT}",
- icon=_extract_node_icon(full_schema[InfrahubKind.ARTIFACT]),
- ),
- InterfaceMenu(
- title="Artifact Definition",
- path=f"/objects/{InfrahubKind.ARTIFACTDEFINITION}",
- icon=_extract_node_icon(full_schema[InfrahubKind.ARTIFACTDEFINITION]),
- ),
- InterfaceMenu(
- title="Generator Definition",
- path=f"/objects/{InfrahubKind.GENERATORDEFINITION}",
- icon=_extract_node_icon(full_schema[InfrahubKind.GENERATORDEFINITION]),
- ),
- InterfaceMenu(
- title="Generator Instance",
- path=f"/objects/{InfrahubKind.GENERATORINSTANCE}",
- icon=_extract_node_icon(full_schema[InfrahubKind.GENERATORINSTANCE]),
- ),
- InterfaceMenu(
- title="Transformation",
- path=f"/objects/{InfrahubKind.TRANSFORM}",
- icon=_extract_node_icon(full_schema[InfrahubKind.TRANSFORM]),
- ),
- ],
- )
-
- admin = InterfaceMenu(
- title="Admin",
- children=[
- InterfaceMenu(
- title="Role Management",
- path="/role-management",
- icon=_extract_node_icon(full_schema[InfrahubKind.BASEPERMISSION]),
- ),
- InterfaceMenu(
- title="Credentials",
- path=f"/objects/{InfrahubKind.CREDENTIAL}",
- icon=_extract_node_icon(full_schema[InfrahubKind.CREDENTIAL]),
- ),
- InterfaceMenu(
- title="Webhooks",
- children=[
- InterfaceMenu(
- title="Webhook",
- path=f"/objects/{InfrahubKind.STANDARDWEBHOOK}",
- icon=_extract_node_icon(full_schema[InfrahubKind.STANDARDWEBHOOK]),
- ),
- InterfaceMenu(
- title="Custom Webhook",
- path=f"/objects/{InfrahubKind.CUSTOMWEBHOOK}",
- icon=_extract_node_icon(full_schema[InfrahubKind.CUSTOMWEBHOOK]),
- ),
- ],
- ),
- ],
- )
-
- menu_items = [objects]
- if has_ipam:
- menu_items.append(ipam)
- menu_items.extend([groups, unified_storage, change_control, deployment, admin])
-
- return menu_items
-
-
-@router.get("/new")
-async def get_new_menu(
+async def get_menu(
db: InfrahubDatabase = Depends(get_db),
branch: Branch = Depends(get_branch_dep),
account_session: AccountSession = Depends(get_current_user),
) -> Menu:
- log.info("new_menu_request", branch=branch.name)
+ log.info("menu_request", branch=branch.name)
- menu_items = await registry.manager.query(db=db, schema=CoreMenuItem, branch=branch, prefetch_relationships=True)
+ menu_items = await registry.manager.query(
+ db=db, schema=CoreMenuItem, branch=branch
+ ) # , prefetch_relationships=True)
menu = await generate_menu(db=db, branch=branch, account=account_session, menu_items=menu_items)
return menu.to_rest()
diff --git a/backend/infrahub/core/initialization.py b/backend/infrahub/core/initialization.py
index eef11ef412..d2b99d908a 100644
--- a/backend/infrahub/core/initialization.py
+++ b/backend/infrahub/core/initialization.py
@@ -20,7 +20,7 @@
from infrahub.core.node.resource_manager.ip_address_pool import CoreIPAddressPool
from infrahub.core.node.resource_manager.ip_prefix_pool import CoreIPPrefixPool
from infrahub.core.node.resource_manager.number_pool import CoreNumberPool
-from infrahub.core.protocols import CoreAccount, CoreMenuItem
+from infrahub.core.protocols import CoreAccount
from infrahub.core.root import Root
from infrahub.core.schema import SchemaRoot, core_models, internal_schema
from infrahub.core.schema.manager import SchemaManager
@@ -28,7 +28,7 @@
from infrahub.exceptions import DatabaseError
from infrahub.log import get_logger
from infrahub.menu.menu import default_menu
-from infrahub.menu.models import MenuItemDefinition
+from infrahub.menu.utils import create_menu_children
from infrahub.permissions import PermissionBackend
from infrahub.storage import InfrahubObjectStorage
from infrahub.utils import format_label
@@ -307,14 +307,6 @@ async def create_initial_permission(db: InfrahubDatabase) -> Node:
return permission
-async def create_menu_children(db: InfrahubDatabase, parent: CoreMenuItem, children: list[MenuItemDefinition]) -> None:
- for child in children:
- obj = await child.to_node(db=db, parent=parent)
- await obj.save(db=db)
- if child.children:
- await create_menu_children(db=db, parent=obj, children=child.children)
-
-
async def create_default_menu(db: InfrahubDatabase) -> None:
for item in default_menu:
obj = await item.to_node(db=db)
diff --git a/backend/infrahub/core/protocols.py b/backend/infrahub/core/protocols.py
index a49de3ff52..f9976574af 100644
--- a/backend/infrahub/core/protocols.py
+++ b/backend/infrahub/core/protocols.py
@@ -137,6 +137,7 @@ class CoreMenu(CoreNode):
namespace: String
name: String
label: StringOptional
+ kind: StringOptional
path: StringOptional
description: StringOptional
icon: StringOptional
diff --git a/backend/infrahub/core/schema/definitions/core.py b/backend/infrahub/core/schema/definitions/core.py
index c839c1ccf6..319830e417 100644
--- a/backend/infrahub/core/schema/definitions/core.py
+++ b/backend/infrahub/core/schema/definitions/core.py
@@ -67,11 +67,12 @@
"description": "Base node for the menu",
"label": "Menu Item",
"hierarchical": True,
- "uniqueness_constraints": [["namespace__value", "name__value"]],
+ "human_friendly_id": ["namespace__value", "name__value"],
"attributes": [
{"name": "namespace", "kind": "Text", "regex": NAMESPACE_REGEX, "order_weight": 1000},
{"name": "name", "kind": "Text", "order_weight": 1000},
{"name": "label", "kind": "Text", "optional": True, "order_weight": 2000},
+ {"name": "kind", "kind": "Text", "optional": True, "order_weight": 2500},
{"name": "path", "kind": "Text", "optional": True, "order_weight": 2500},
{"name": "description", "kind": "Text", "optional": True, "order_weight": 3000},
{"name": "icon", "kind": "Text", "optional": True, "order_weight": 4000},
diff --git a/backend/infrahub/core/schema/schema_branch.py b/backend/infrahub/core/schema/schema_branch.py
index 116cf5ca42..5d52e13dad 100644
--- a/backend/infrahub/core/schema/schema_branch.py
+++ b/backend/infrahub/core/schema/schema_branch.py
@@ -486,7 +486,6 @@ def process_pre_validation(self) -> None:
def process_validate(self) -> None:
self.validate_names()
- self.validate_menu_placements()
self.validate_kinds()
self.validate_default_values()
self.validate_count_against_cardinality()
@@ -899,28 +898,6 @@ def validate_names(self) -> None:
):
raise ValueError(f"{node.kind}: {rel.name} isn't allowed as a relationship name.")
- def validate_menu_placements(self) -> None:
- menu_placements: dict[str, str] = {}
-
- for name in list(self.nodes.keys()) + list(self.generics.keys()):
- node = self.get(name=name, duplicate=False)
- if node.menu_placement:
- try:
- placement_node = self.get(name=node.menu_placement, duplicate=False)
- except SchemaNotFoundError as exc:
- raise SchemaNotFoundError(
- branch_name=self.name,
- identifier=node.menu_placement,
- message=f"{node.kind} refers to an invalid menu placement node: {node.menu_placement}.",
- ) from exc
- if node == placement_node:
- raise ValueError(f"{node.kind}: cannot be placed under itself in the menu") from None
-
- if menu_placements.get(placement_node.kind) == node.kind:
- raise ValueError(f"{node.kind}: cyclic menu placement with {placement_node.kind}") from None
-
- menu_placements[node.kind] = placement_node.kind
-
def validate_kinds(self) -> None:
for name in list(self.nodes.keys()):
node = self.get_node(name=name, duplicate=False)
diff --git a/backend/infrahub/menu/generator.py b/backend/infrahub/menu/generator.py
index 485d3dbf52..49645bba7f 100644
--- a/backend/infrahub/menu/generator.py
+++ b/backend/infrahub/menu/generator.py
@@ -32,31 +32,29 @@ async def generate_menu(
full_schema = registry.schema.get_full(branch=branch, duplicate=False)
already_processed = []
- havent_been_processed = []
# Process the parent first
for item in menu_items:
full_name = get_full_name(item)
- parent = await item.parent.get_peer(db=db, peer_type=CoreMenuItem)
- if parent:
- havent_been_processed.append(full_name)
+ parent1 = await item.parent.get_peer(db=db, peer_type=CoreMenuItem)
+ if parent1:
continue
structure.data[full_name] = MenuItemDict.from_node(obj=item)
already_processed.append(full_name)
# Process the children
- havent_been_processed = []
+ havent_been_processed: list[str] = []
for item in menu_items:
full_name = get_full_name(item)
if full_name in already_processed:
continue
- parent = await item.parent.get_peer(db=db, peer_type=CoreMenuItem)
- if not parent:
+ parent2 = await item.parent.get_peer(db=db, peer_type=CoreMenuItem)
+ if not parent2:
havent_been_processed.append(full_name)
continue
- parent_full_name = get_full_name(parent)
+ parent_full_name = get_full_name(parent2)
menu_item = structure.find_item(name=parent_full_name)
if menu_item:
child_item = MenuItemDict.from_node(obj=item)
@@ -65,8 +63,8 @@ async def generate_menu(
log.warning(
"new_menu_request: unable to find the parent menu item",
branch=branch.name,
- menu_item=item.name.value,
- parent_item=parent.name.value,
+ menu_item=child_item.identifier,
+ parent_item=parent_full_name,
)
default_menu = structure.find_item(name=FULL_DEFAULT_MENU)
diff --git a/backend/infrahub/menu/menu.py b/backend/infrahub/menu/menu.py
index cbc4576594..d35e9bcb9d 100644
--- a/backend/infrahub/menu/menu.py
+++ b/backend/infrahub/menu/menu.py
@@ -27,7 +27,62 @@ def _extract_node_icon(model: MainSchemaTypes) -> str:
name=DEFAULT_MENU,
label=DEFAULT_MENU.title(),
protected=True,
+ icon="mdi:cube-outline",
+ section=MenuSection.OBJECT,
+ order_weight=10000,
+ ),
+ MenuItemDefinition(
+ namespace="Builtin",
+ name="IPAM",
+ label="IPAM",
+ protected=True,
section=MenuSection.OBJECT,
+ icon="mdi:ip-network",
+ order_weight=9500,
+ children=[
+ MenuItemDefinition(
+ namespace="Builtin",
+ name="IPPrefix",
+ label="IP Prefixes",
+ kind=InfrahubKind.IPPREFIX,
+ path="/ipam/prefixes",
+ icon=_extract_node_icon(infrahub_schema.get(InfrahubKind.IPPREFIX)),
+ protected=True,
+ section=MenuSection.INTERNAL,
+ order_weight=1000,
+ ),
+ MenuItemDefinition(
+ namespace="Builtin",
+ name="IPAddress",
+ label="IP Addresses",
+ kind=InfrahubKind.IPPREFIX,
+ path="/ipam/addresses?ipam-tab=ip-details",
+ icon=_extract_node_icon(infrahub_schema.get(InfrahubKind.IPADDRESS)),
+ protected=True,
+ section=MenuSection.INTERNAL,
+ order_weight=2000,
+ ),
+ MenuItemDefinition(
+ namespace="Builtin",
+ name="Namespaces",
+ label="Namespaces",
+ kind=InfrahubKind.IPNAMESPACE,
+ icon=_extract_node_icon(infrahub_schema.get(InfrahubKind.IPNAMESPACE)),
+ protected=True,
+ section=MenuSection.INTERNAL,
+ order_weight=3000,
+ ),
+ ],
+ ),
+ MenuItemDefinition(
+ namespace="Builtin",
+ name="ProposedChanges",
+ label="Proposed Changes",
+ path="/proposed-changes",
+ icon=_extract_node_icon(infrahub_schema.get(InfrahubKind.PROPOSEDCHANGE)),
+ protected=True,
+ section=MenuSection.INTERNAL,
+ order_weight=1000,
),
MenuItemDefinition(
namespace="Builtin",
@@ -36,7 +91,7 @@ def _extract_node_icon(model: MainSchemaTypes) -> str:
icon="mdi:cube-outline",
protected=True,
section=MenuSection.INTERNAL,
- order_weight=1000,
+ order_weight=1500,
children=[
MenuItemDefinition(
namespace="Builtin",
@@ -74,7 +129,7 @@ def _extract_node_icon(model: MainSchemaTypes) -> str:
namespace="Builtin",
name="ChangeControl",
label="Change Control",
- icon="mdi:compare-vertical",
+ icon="mdi:source-branch",
protected=True,
section=MenuSection.INTERNAL,
order_weight=2000,
@@ -89,16 +144,6 @@ def _extract_node_icon(model: MainSchemaTypes) -> str:
section=MenuSection.INTERNAL,
order_weight=1000,
),
- MenuItemDefinition(
- namespace="Builtin",
- name="ProposedChanges",
- label="Proposed Changes",
- path="/proposed-changes",
- icon=_extract_node_icon(infrahub_schema.get(InfrahubKind.PROPOSEDCHANGE)),
- protected=True,
- section=MenuSection.INTERNAL,
- order_weight=2000,
- ),
MenuItemDefinition(
namespace="Builtin",
name="CheckDefinition",
@@ -121,11 +166,92 @@ def _extract_node_icon(model: MainSchemaTypes) -> str:
),
],
),
+ MenuItemDefinition(
+ namespace="Builtin",
+ name="Deployment",
+ label="Deployment",
+ icon="mdi:rocket-launch",
+ protected=True,
+ section=MenuSection.INTERNAL,
+ order_weight=2500,
+ children=[
+ MenuItemDefinition(
+ namespace="Builtin",
+ name="ArtifactMenu",
+ label="Artifact",
+ protected=True,
+ section=MenuSection.INTERNAL,
+ order_weight=1000,
+ children=[
+ MenuItemDefinition(
+ namespace="Builtin",
+ name="Artifact",
+ label="Artifact",
+ kind=InfrahubKind.ARTIFACT,
+ icon=_extract_node_icon(infrahub_schema.get(InfrahubKind.ARTIFACT)),
+ protected=True,
+ section=MenuSection.INTERNAL,
+ order_weight=1000,
+ ),
+ MenuItemDefinition(
+ namespace="Builtin",
+ name="ArtifactDefinition",
+ label="Artifact Definition",
+ kind=InfrahubKind.ARTIFACTDEFINITION,
+ icon=_extract_node_icon(infrahub_schema.get(InfrahubKind.ARTIFACTDEFINITION)),
+ protected=True,
+ section=MenuSection.INTERNAL,
+ order_weight=1000,
+ ),
+ ],
+ ),
+ MenuItemDefinition(
+ namespace="Builtin",
+ name="GeneratorMenu",
+ label="Generator",
+ protected=True,
+ section=MenuSection.INTERNAL,
+ order_weight=1000,
+ children=[
+ MenuItemDefinition(
+ namespace="Builtin",
+ name="GeneratorInstance",
+ label="Generator Instance",
+ kind=InfrahubKind.GENERATORINSTANCE,
+ icon=_extract_node_icon(infrahub_schema.get(InfrahubKind.GENERATORINSTANCE)),
+ protected=True,
+ section=MenuSection.INTERNAL,
+ order_weight=1000,
+ ),
+ MenuItemDefinition(
+ namespace="Builtin",
+ name="GeneratorDefinition",
+ label="Generator Definition",
+ kind=InfrahubKind.GENERATORDEFINITION,
+ icon=_extract_node_icon(infrahub_schema.get(InfrahubKind.GENERATORDEFINITION)),
+ protected=True,
+ section=MenuSection.INTERNAL,
+ order_weight=2000,
+ ),
+ ],
+ ),
+ MenuItemDefinition(
+ namespace="Builtin",
+ name="Transformation",
+ label="Transformation",
+ kind=InfrahubKind.TRANSFORM,
+ icon=_extract_node_icon(infrahub_schema.get(InfrahubKind.TRANSFORM)),
+ protected=True,
+ section=MenuSection.INTERNAL,
+ order_weight=3000,
+ ),
+ ],
+ ),
MenuItemDefinition(
namespace="Builtin",
name="UnifiedStorage",
label="Unified Storage",
- icon="mdi:archive-arrow-down-outline",
+ icon="mdi:nas",
protected=True,
section=MenuSection.INTERNAL,
order_weight=3000,
@@ -142,7 +268,7 @@ def _extract_node_icon(model: MainSchemaTypes) -> str:
),
MenuItemDefinition(
namespace="Builtin",
- name="Repository",
+ name="Git Repository",
label="Repository",
kind=InfrahubKind.GENERICREPOSITORY,
icon=_extract_node_icon(infrahub_schema.get(InfrahubKind.GENERICREPOSITORY)),
@@ -169,7 +295,7 @@ def _extract_node_icon(model: MainSchemaTypes) -> str:
icon="mdi:settings-outline",
protected=True,
section=MenuSection.INTERNAL,
- order_weight=3000,
+ order_weight=10000,
children=[
MenuItemDefinition(
namespace="Builtin",
@@ -225,35 +351,3 @@ def _extract_node_icon(model: MainSchemaTypes) -> str:
],
),
]
-
-
-# deployment = InterfaceMenu(
-# title="Deployment",
-# children=[
-# InterfaceMenu(
-# title="Artifact",
-# kind=InfrahubKind.ARTIFACT}",
-# icon=_extract_node_icon(full_schema[InfrahubKind.ARTIFACT]),
-# ),
-# InterfaceMenu(
-# title="Artifact Definition",
-# kind=InfrahubKind.ARTIFACTDEFINITION}",
-# icon=_extract_node_icon(full_schema[InfrahubKind.ARTIFACTDEFINITION]),
-# ),
-# InterfaceMenu(
-# title="Generator Definition",
-# kind=InfrahubKind.GENERATORDEFINITION}",
-# icon=_extract_node_icon(full_schema[InfrahubKind.GENERATORDEFINITION]),
-# ),
-# InterfaceMenu(
-# title="Generator Instance",
-# kind=InfrahubKind.GENERATORINSTANCE}",
-# icon=_extract_node_icon(full_schema[InfrahubKind.GENERATORINSTANCE]),
-# ),
-# InterfaceMenu(
-# title="Transformation",
-# kind=InfrahubKind.TRANSFORM}",
-# icon=_extract_node_icon(full_schema[InfrahubKind.TRANSFORM]),
-# ),
-# ],
-# )
diff --git a/backend/infrahub/menu/models.py b/backend/infrahub/menu/models.py
index f5e2ac94ba..db0b17ce1e 100644
--- a/backend/infrahub/menu/models.py
+++ b/backend/infrahub/menu/models.py
@@ -72,7 +72,7 @@ class Menu:
class MenuItem(BaseModel):
identifier: str = Field(..., description="Unique identifier for this menu item")
- title: str = Field(..., description="Title of the menu item")
+ label: str = Field(..., description="Title of the menu item")
path: str = Field(default="", description="URL endpoint if applicable")
icon: str = Field(default="", description="The icon to show for the current view")
kind: str = Field(default="", description="Kind of the model associated with this menuitem if applicable")
@@ -83,11 +83,11 @@ class MenuItem(BaseModel):
def from_node(cls, obj: CoreMenuItem) -> Self:
return cls(
identifier=get_full_name(obj),
- title=obj.label.value or "",
+ label=obj.label.value or "",
icon=obj.icon.value or "",
order_weight=obj.order_weight.value,
path=obj.path.value or "",
- kind=obj.get_kind(),
+ kind=obj.kind.value or "",
section=obj.section.value,
)
@@ -95,7 +95,7 @@ def from_node(cls, obj: CoreMenuItem) -> Self:
def from_schema(cls, model: NodeSchema | GenericSchema | ProfileSchema) -> Self:
return cls(
identifier=get_full_name(model),
- title=model.label or model.kind,
+ label=model.label or model.kind,
path=f"/objects/{model.kind}",
icon=model.icon or "",
kind=model.kind,
diff --git a/backend/infrahub/menu/utils.py b/backend/infrahub/menu/utils.py
new file mode 100644
index 0000000000..da00d11bac
--- /dev/null
+++ b/backend/infrahub/menu/utils.py
@@ -0,0 +1,12 @@
+from infrahub.core.protocols import CoreMenuItem
+from infrahub.database import InfrahubDatabase
+
+from .models import MenuItemDefinition
+
+
+async def create_menu_children(db: InfrahubDatabase, parent: CoreMenuItem, children: list[MenuItemDefinition]) -> None:
+ for child in children:
+ obj = await child.to_node(db=db, parent=parent)
+ await obj.save(db=db)
+ if child.children:
+ await create_menu_children(db=db, parent=obj, children=child.children)
diff --git a/backend/tests/benchmark/test_get_menu.py b/backend/tests/benchmark/test_get_menu.py
index c4aea5013b..c4c944300b 100644
--- a/backend/tests/benchmark/test_get_menu.py
+++ b/backend/tests/benchmark/test_get_menu.py
@@ -1,6 +1,14 @@
+import pytest
+
from infrahub.api.menu import get_menu
+from infrahub.core.initialization import create_default_menu
from infrahub.database import InfrahubDatabase
-def test_get_menu(aio_benchmark, db: InfrahubDatabase, default_branch, register_core_models_schema):
- aio_benchmark(get_menu, branch=default_branch)
+@pytest.fixture
+async def init_menu(db: InfrahubDatabase, default_branch, register_core_models_schema):
+ await create_default_menu(db=db)
+
+
+def test_get_menu(aio_benchmark, db: InfrahubDatabase, default_branch, register_core_models_schema, init_menu):
+ aio_benchmark(get_menu, db=db, branch=default_branch, account_session=None)
diff --git a/backend/tests/integration/schema_lifecycle/test_schema_missing_menu_placement.py b/backend/tests/integration/schema_lifecycle/test_schema_missing_menu_placement.py
deleted file mode 100644
index 076ea39112..0000000000
--- a/backend/tests/integration/schema_lifecycle/test_schema_missing_menu_placement.py
+++ /dev/null
@@ -1,30 +0,0 @@
-from infrahub_sdk import InfrahubClient
-
-from .shared import (
- TestSchemaLifecycleBase,
-)
-
-
-class TestSchemaMissingMenuPlacement(TestSchemaLifecycleBase):
- async def test_schema_missing_menu_placement(self, client: InfrahubClient):
- schema = {
- "version": "1.0",
- "nodes": [
- {
- "name": "BNode",
- "namespace": "Infra",
- "menu_placement": "UnexistingNode",
- "label": "BNode",
- "display_labels": ["name__value"],
- "attributes": [{"name": "name", "kind": "Text", "unique": True}],
- }
- ],
- }
-
- response = await client.schema.load(schemas=[schema], branch="main")
- assert response.schema_updated is False
- assert response.errors["errors"][0]["extensions"]["code"] == 422
- assert (
- response.errors["errors"][0]["message"]
- == "InfraBNode refers to an invalid menu placement node: UnexistingNode."
- )
diff --git a/backend/tests/unit/api/test_menu.py b/backend/tests/unit/api/test_menu.py
index 4d37efaf70..17157946b9 100644
--- a/backend/tests/unit/api/test_menu.py
+++ b/backend/tests/unit/api/test_menu.py
@@ -1,4 +1,3 @@
-from infrahub.api.menu import InterfaceMenu
from infrahub.core.branch import Branch
from infrahub.core.initialization import create_default_menu
from infrahub.core.schema import SchemaRoot
@@ -12,34 +11,12 @@ async def test_get_menu(
default_branch: Branch,
car_person_schema_generics: SchemaRoot,
car_person_data_generic,
-):
- with client:
- response = client.get(
- "/api/menu",
- headers=client_headers,
- )
-
- assert response.status_code == 200
- assert response.json() is not None
-
- menu = [InterfaceMenu(**menu_item) for menu_item in response.json()]
- assert menu[0].title == "Objects"
- assert menu[0].children[0].title == "Car"
-
-
-async def test_get_new_menu(
- db: InfrahubDatabase,
- client,
- client_headers,
- default_branch: Branch,
- car_person_schema_generics: SchemaRoot,
- car_person_data_generic,
):
await create_default_menu(db=db)
with client:
response = client.get(
- "/api/menu/new",
+ "/api/menu",
headers=client_headers,
)
diff --git a/backend/tests/unit/core/schema_manager/test_manager_schema.py b/backend/tests/unit/core/schema_manager/test_manager_schema.py
index 23ad98c2f1..0179f7938e 100644
--- a/backend/tests/unit/core/schema_manager/test_manager_schema.py
+++ b/backend/tests/unit/core/schema_manager/test_manager_schema.py
@@ -920,115 +920,6 @@ async def test_schema_branch_validate_kinds_core(register_core_models_schema: Sc
register_core_models_schema.validate_kinds()
-async def test_schema_branch_validate_menu_placement():
- """Validate that menu placements points to objects that exists in the schema."""
- FULL_SCHEMA = {
- "version": "1.0",
- "nodes": [
- {
- "name": "Criticality",
- "namespace": "Test",
- "default_filter": "name__value",
- "branch": BranchSupportType.AWARE.value,
- "attributes": [
- {"name": "name", "kind": "Text", "unique": True},
- ],
- },
- {
- "name": "SubObject",
- "namespace": "Test",
- "menu_placement": "NoSuchObject",
- "default_filter": "name__value",
- "branch": BranchSupportType.AWARE.value,
- "attributes": [
- {"name": "name", "kind": "Text", "unique": True},
- ],
- },
- ],
- }
-
- schema = SchemaBranch(cache={})
- schema.load_schema(schema=SchemaRoot(**FULL_SCHEMA))
-
- with pytest.raises(SchemaNotFoundError) as exc:
- schema.validate_menu_placements()
-
- assert exc.value.message == "TestSubObject refers to an invalid menu placement node: NoSuchObject."
-
-
-async def test_schema_branch_validate_same_node_menu_placement():
- """Validate that menu placements points to objects that exists in the schema."""
- FULL_SCHEMA = {
- "version": "1.0",
- "nodes": [
- {
- "name": "Criticality",
- "namespace": "Test",
- "default_filter": "name__value",
- "branch": BranchSupportType.AWARE.value,
- "attributes": [
- {"name": "name", "kind": "Text", "unique": True},
- ],
- },
- {
- "name": "SubObject",
- "namespace": "Test",
- "menu_placement": "TestSubObject",
- "default_filter": "name__value",
- "branch": BranchSupportType.AWARE.value,
- "attributes": [
- {"name": "name", "kind": "Text", "unique": True},
- ],
- },
- ],
- }
-
- schema = SchemaBranch(cache={})
- schema.load_schema(schema=SchemaRoot(**FULL_SCHEMA))
-
- with pytest.raises(ValueError) as exc:
- schema.validate_menu_placements()
-
- assert str(exc.value) == "TestSubObject: cannot be placed under itself in the menu"
-
-
-async def test_schema_branch_validate_cyclic_menu_placement():
- """Validate that menu placements points to objects that exists in the schema."""
- FULL_SCHEMA = {
- "version": "1.0",
- "nodes": [
- {
- "name": "Criticality",
- "namespace": "Test",
- "menu_placement": "TestSubObject",
- "default_filter": "name__value",
- "branch": BranchSupportType.AWARE.value,
- "attributes": [
- {"name": "name", "kind": "Text", "unique": True},
- ],
- },
- {
- "name": "SubObject",
- "namespace": "Test",
- "menu_placement": "TestCriticality",
- "default_filter": "name__value",
- "branch": BranchSupportType.AWARE.value,
- "attributes": [
- {"name": "name", "kind": "Text", "unique": True},
- ],
- },
- ],
- }
-
- schema = SchemaBranch(cache={})
- schema.load_schema(schema=SchemaRoot(**FULL_SCHEMA))
-
- with pytest.raises(ValueError) as exc:
- schema.validate_menu_placements()
-
- assert str(exc.value) == "TestSubObject: cyclic menu placement with TestCriticality"
-
-
@pytest.mark.parametrize(
"uniqueness_constraints",
[
diff --git a/backend/tests/unit/menu/test_generator.py b/backend/tests/unit/menu/test_generator.py
index 1781ae4c02..7d18927e26 100644
--- a/backend/tests/unit/menu/test_generator.py
+++ b/backend/tests/unit/menu/test_generator.py
@@ -7,12 +7,37 @@
from infrahub.menu.constants import MenuSection
from infrahub.menu.generator import generate_menu
from infrahub.menu.models import MenuItemDefinition
+from infrahub.menu.utils import create_menu_children
+
+
+def generate_menu_fixtures(prefix: str = "Menu", depth: int = 1, nbr_item: int = 10) -> list[MenuItemDefinition]:
+ max_depth = 3
+ next_level_item: int = 5
+
+ menu: list[MenuItemDefinition] = []
+
+ for idx in range(nbr_item):
+ item = MenuItemDefinition(
+ namespace="Test",
+ name=f"{prefix}{idx}",
+ label=f"{prefix}{idx}",
+ section=MenuSection.OBJECT,
+ order_weight=(idx + 1) * 1000,
+ )
+
+ if depth <= max_depth:
+ item.children = generate_menu_fixtures(prefix=f"{prefix}{idx}", depth=depth + 1, nbr_item=next_level_item)
+
+ menu.append(item)
+
+ return menu
async def test_generate_menu(
db: InfrahubDatabase,
default_branch: Branch,
car_person_schema_generics: SchemaRoot,
+ helper,
):
schema_branch = registry.schema.get_schema_branch(name=default_branch.name)
@@ -22,20 +47,13 @@ async def test_generate_menu(
await create_default_menu(db=db)
- new_menu_items = [
- MenuItemDefinition(
- namespace="Test",
- name="CarGaz",
- label="Car Gaz",
- kind="TestCarGaz",
- section=MenuSection.OBJECT,
- order_weight=1500,
- )
- ]
+ new_menu_items = generate_menu_fixtures(nbr_item=5)
for item in new_menu_items:
obj = await item.to_node(db=db)
await obj.save(db=db)
+ if item.children:
+ await create_menu_children(db=db, parent=obj, children=item.children)
menu_items = await registry.manager.query(
db=db, schema=CoreMenuItem, branch=default_branch, prefetch_relationships=True
@@ -43,4 +61,4 @@ async def test_generate_menu(
menu = await generate_menu(db=db, branch=default_branch, menu_items=menu_items)
assert menu
- assert "Test:CarGaz" in menu.data.keys()
+ assert "Test:Menu0" in menu.data.keys()
diff --git a/frontend/app/src/components/search/search-actions.tsx b/frontend/app/src/components/search/search-actions.tsx
index b0a852735d..ee99d22e92 100644
--- a/frontend/app/src/components/search/search-actions.tsx
+++ b/frontend/app/src/components/search/search-actions.tsx
@@ -17,8 +17,8 @@ export const SearchActions = ({ query }: SearchProps) => {
const menuItems = useAtomValue(menuFlatAtom);
const queryLowerCased = query.toLowerCase();
- const resultsMenu = menuItems.filter(({ title }) =>
- title.toLowerCase().includes(queryLowerCased)
+ const resultsMenu = menuItems.filter(({ label }) =>
+ label.toLowerCase().includes(queryLowerCased)
);
const resultsSchema = models.filter(
({ kind, label, description }) =>
@@ -55,7 +55,7 @@ const ActionOnMenu = ({ menuItem }: ActionOnMenuProps) => {