Skip to content

Commit

Permalink
Add new menu model and endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
dgarros committed Oct 7, 2024
1 parent 74fd1b4 commit f288cc6
Show file tree
Hide file tree
Showing 22 changed files with 943 additions and 10 deletions.
22 changes: 21 additions & 1 deletion backend/infrahub/api/menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,21 @@
from fastapi import APIRouter, Depends
from pydantic import BaseModel, Field

from infrahub.api.dependencies import get_branch_dep
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


log = get_logger()
router = APIRouter(prefix="/menu")
Expand Down Expand Up @@ -231,3 +237,17 @@ async def get_menu(branch: Branch = Depends(get_branch_dep)) -> list[InterfaceMe
menu_items.extend([groups, unified_storage, change_control, deployment, admin])

return menu_items


@router.get("/new")
async def get_new_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)

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()
1 change: 1 addition & 0 deletions backend/infrahub/core/constants/infrahubkind.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
IPADDRESSPOOL = "CoreIPAddressPool"
IPPREFIX = "BuiltinIPPrefix"
IPPREFIXPOOL = "CoreIPPrefixPool"
MENUITEM = "CoreMenuItem"
NAMESPACE = "IpamNamespace"
NODE = "CoreNode"
NUMBERPOOL = "CoreNumberPool"
Expand Down
25 changes: 24 additions & 1 deletion backend/infrahub/core/initialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@
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
from infrahub.core.protocols import CoreAccount, CoreMenuItem
from infrahub.core.root import Root
from infrahub.core.schema import SchemaRoot, core_models, internal_schema
from infrahub.core.schema.manager import SchemaManager
from infrahub.database import InfrahubDatabase
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.permissions import PermissionBackend
from infrahub.storage import InfrahubObjectStorage
from infrahub.utils import format_label
Expand Down Expand Up @@ -305,6 +307,22 @@ 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)
await obj.save(db=db)
if item.children:
await create_menu_children(db=db, parent=obj, children=item.children)


async def create_super_administrator_role(db: InfrahubDatabase) -> Node:
permission = await Node.init(db=db, schema=InfrahubKind.GLOBALPERMISSION)
await permission.new(
Expand Down Expand Up @@ -364,6 +382,11 @@ async def first_time_initialization(db: InfrahubDatabase) -> None:
await default_branch.save(db=db)
log.info("Created the Schema in the database", hash=default_branch.active_schema_hash.main)

# --------------------------------------------------
# Create Default Menu
# --------------------------------------------------
await create_default_menu(db=db)

# --------------------------------------------------
# Create Default Users and Groups
# --------------------------------------------------
Expand Down
15 changes: 11 additions & 4 deletions backend/infrahub/core/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -1043,21 +1043,28 @@ async def get_one(
node = result[id]
node_schema = node.get_schema()

kind_validation = None
if kind:
node_schema_validation = get_schema(db=db, branch=branch, node_schema=kind)
kind_validation = node_schema_validation.kind

# Temporary list of exception to the validation of the kind
kind_validation_exceptions = [
("CoreChangeThread", "CoreObjectThread"), # issue/3318
]

if kind and (node_schema.kind != kind and kind not in node_schema.inherit_from):
if kind_validation and (
node_schema.kind != kind_validation and kind_validation not in node_schema.inherit_from
):
for item in kind_validation_exceptions:
if item[0] == kind and item[1] == node.get_kind():
if item[0] == kind_validation and item[1] == node.get_kind():
return node

raise NodeNotFoundError(
branch_name=branch.name,
node_type=kind,
node_type=kind_validation,
identifier=id,
message=f"Node with id {id} exists, but it is a {node.get_kind()}, not {kind}",
message=f"Node with id {id} exists, but it is a {node.get_kind()}, not {kind_validation}",
)

return node
Expand Down
18 changes: 18 additions & 0 deletions backend/infrahub/core/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,20 @@ class CoreGroup(CoreNode):
children: RelationshipManager


class CoreMenu(CoreNode):
namespace: String
name: String
label: StringOptional
path: StringOptional
description: StringOptional
icon: StringOptional
protected: Boolean
order_weight: Integer
section: Enum
parent: RelationshipManager
children: RelationshipManager


class CoreProfile(CoreNode):
profile_name: String
profile_priority: IntegerOptional
Expand Down Expand Up @@ -365,6 +379,10 @@ class CoreIPPrefixPool(CoreResourcePool, LineageSource):
ip_namespace: RelationshipManager


class CoreMenuItem(CoreMenu):
pass


class CoreNumberPool(CoreResourcePool, LineageSource):
node: String
node_attribute: String
Expand Down
10 changes: 10 additions & 0 deletions backend/infrahub/core/schema/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from infrahub.core.constants import RESTRICTED_NAMESPACES
from infrahub.core.models import HashableModel
from infrahub.exceptions import SchemaNotFoundError

from .attribute_schema import AttributeSchema
from .basenode_schema import AttributePathParsingError, BaseNodeSchema, SchemaAttributePath, SchemaAttributePathValue
Expand Down Expand Up @@ -57,6 +58,15 @@ def has_schema(cls, values: dict[str, Any], name: str) -> bool:

return True

def get(self, name: str) -> Union[NodeSchema, GenericSchema]:
"""Check if a schema exist locally as a node or as a generic."""

for item in self.nodes + self.generics:
if item.kind == name:
return item

raise SchemaNotFoundError(branch_name="undefined", identifier=name)

def validate_namespaces(self) -> list[str]:
models = self.nodes + self.generics
errors: list[str] = []
Expand Down
41 changes: 41 additions & 0 deletions backend/infrahub/core/schema/definitions/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from infrahub.core.constants import (
DEFAULT_KIND_MAX_LENGTH,
DEFAULT_KIND_MIN_LENGTH,
NAMESPACE_REGEX,
AccountRole,
AccountStatus,
AccountType,
Expand Down Expand Up @@ -56,6 +57,44 @@
],
}

# -----------------------------------------------
# Menu Items
# -----------------------------------------------
generic_menu_item: dict[str, Any] = {
"name": "Menu",
"namespace": "Core",
"include_in_menu": False,
"description": "Base node for the menu",
"label": "Menu Item",
"hierarchical": True,
"uniqueness_constraints": [["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": "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},
{"name": "protected", "kind": "Boolean", "default_value": False, "read_only": True, "order_weight": 5000},
{"name": "order_weight", "kind": "Number", "default_value": 2000, "order_weight": 6000},
{
"name": "section",
"kind": "Text",
"enum": ["object", "internal"],
"default_value": "object",
"order_weight": 7000,
},
],
}

menu_item: dict[str, Any] = {
"name": "MenuItem",
"namespace": "Core",
"include_in_menu": False,
"description": "Menu Item",
"label": "Menu Item",
"inherit_from": ["CoreMenu"],
}

core_models: dict[str, Any] = {
"generics": [
Expand Down Expand Up @@ -938,8 +977,10 @@
{"name": "description", "kind": "Text", "optional": True, "order_weight": 3000},
],
},
generic_menu_item,
],
"nodes": [
menu_item,
{
"name": "StandardGroup",
"namespace": "Core",
Expand Down
2 changes: 2 additions & 0 deletions backend/infrahub/graphql/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
InfrahubIPPrefixMutation,
)
from .mutations.main import InfrahubMutation
from .mutations.menu import InfrahubCoreMenuMutation
from .mutations.proposed_change import InfrahubProposedChangeMutation
from .mutations.repository import InfrahubRepositoryMutation
from .mutations.resource_manager import (
Expand Down Expand Up @@ -415,6 +416,7 @@ def generate_mutation_mixin(self) -> type[object]:
InfrahubKind.GRAPHQLQUERY: InfrahubGraphQLQueryMutation,
InfrahubKind.NAMESPACE: InfrahubIPNamespaceMutation,
InfrahubKind.NUMBERPOOL: InfrahubNumberPoolMutation,
InfrahubKind.MENUITEM: InfrahubCoreMenuMutation,
}

if isinstance(node_schema, NodeSchema) and node_schema.is_ip_prefix():
Expand Down
2 changes: 1 addition & 1 deletion backend/infrahub/graphql/mutations/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ async def mutate_delete(
data: InputObjectType,
branch: Branch,
at: str,
):
) -> tuple[Node, Self]:
context: GraphqlContext = info.context

obj = await NodeManager.find_object(
Expand Down
103 changes: 103 additions & 0 deletions backend/infrahub/graphql/mutations/menu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
from typing import TYPE_CHECKING, Any, Optional

from graphene import InputObjectType, Mutation
from graphql import GraphQLResolveInfo
from typing_extensions import Self

from infrahub.core.branch import Branch
from infrahub.core.constants import RESTRICTED_NAMESPACES
from infrahub.core.manager import NodeManager
from infrahub.core.node import Node
from infrahub.core.protocols import CoreMenuItem
from infrahub.core.schema import NodeSchema
from infrahub.database import InfrahubDatabase
from infrahub.exceptions import ValidationError
from infrahub.graphql.mutations.main import InfrahubMutationMixin

from .main import InfrahubMutationOptions

if TYPE_CHECKING:
from infrahub.graphql.initialization import GraphqlContext

EXTENDED_RESTRICTED_NAMESPACES = RESTRICTED_NAMESPACES + ["Builtin"]


def validate_namespace(data: InputObjectType) -> None:
namespace = data.get("namespace")
if isinstance(namespace, dict) and "value" in namespace:
namespace_value = str(namespace.get("value"))
if namespace_value in EXTENDED_RESTRICTED_NAMESPACES:
raise ValidationError(
input_value={"namespace": f"{namespace_value} is not valid, it's a restricted namespace"}
)


class InfrahubCoreMenuMutation(InfrahubMutationMixin, Mutation):
@classmethod
def __init_subclass_with_meta__( # pylint: disable=arguments-differ
cls, schema: NodeSchema, _meta: Optional[Any] = None, **options: dict[str, Any]
) -> None:
# Make sure schema is a valid NodeSchema Node Class
if not isinstance(schema, NodeSchema):
raise ValueError(f"You need to pass a valid NodeSchema in '{cls.__name__}.Meta', received '{schema}'")

if not _meta:
_meta = InfrahubMutationOptions(cls)
_meta.schema = schema

super().__init_subclass_with_meta__(_meta=_meta, **options)

@classmethod
async def mutate_create(
cls,
info: GraphQLResolveInfo,
data: InputObjectType,
branch: Branch,
at: str,
database: Optional[InfrahubDatabase] = None,
) -> tuple[Node, Self]:
validate_namespace(data=data)

obj, result = await super().mutate_create(info=info, data=data, branch=branch, at=at)

return obj, result

@classmethod
async def mutate_update(
cls,
info: GraphQLResolveInfo,
data: InputObjectType,
branch: Branch,
at: str,
database: Optional[InfrahubDatabase] = None,
node: Optional[Node] = None,
) -> tuple[Node, Self]:
context: GraphqlContext = info.context

obj = await NodeManager.find_object(
db=context.db, kind=CoreMenuItem, id=data.get("id"), hfid=data.get("hfid"), branch=branch, at=at
)
validate_namespace(data=data)

if obj.protected.value:
raise ValidationError(input_value="This object is protected, it can't be modified.")

obj, result = await super().mutate_update(info=info, data=data, branch=branch, at=at, node=obj) # type: ignore[assignment]
return obj, result # type: ignore[return-value]

@classmethod
async def mutate_delete(
cls,
info: GraphQLResolveInfo,
data: InputObjectType,
branch: Branch,
at: str,
) -> tuple[Node, Self]:
context: GraphqlContext = info.context
obj = await NodeManager.find_object(
db=context.db, kind=CoreMenuItem, id=data.get("id"), hfid=data.get("hfid"), branch=branch, at=at
)
if obj.protected.value:
raise ValidationError(input_value="This object is protected, it can't be deleted.")

return await super().mutate_delete(info=info, data=data, branch=branch, at=at)
Empty file.
10 changes: 10 additions & 0 deletions backend/infrahub/menu/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from infrahub.utils import InfrahubStringEnum


class MenuSection(InfrahubStringEnum):
OBJECT = "object"
INTERNAL = "internal"


DEFAULT_MENU = "Other"
FULL_DEFAULT_MENU = "Builtin:Other"
Loading

0 comments on commit f288cc6

Please sign in to comment.