Skip to content

Commit 8915858

Browse files
authored
Merge pull request #4369 from opsmill/dga-20240918-menu
new menu model and endpoint
2 parents a9887b2 + f288cc6 commit 8915858

File tree

22 files changed

+943
-10
lines changed

22 files changed

+943
-10
lines changed

backend/infrahub/api/menu.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,21 @@
55
from fastapi import APIRouter, Depends
66
from pydantic import BaseModel, Field
77

8-
from infrahub.api.dependencies import get_branch_dep
8+
from infrahub.api.dependencies import get_branch_dep, get_current_user, get_db
99
from infrahub.core import registry
1010
from infrahub.core.branch import Branch # noqa: TCH001
1111
from infrahub.core.constants import InfrahubKind
12+
from infrahub.core.protocols import CoreMenuItem
1213
from infrahub.core.schema import NodeSchema
1314
from infrahub.log import get_logger
15+
from infrahub.menu.generator import generate_menu
16+
from infrahub.menu.models import Menu # noqa: TCH001
1417

1518
if TYPE_CHECKING:
19+
from infrahub.auth import AccountSession
1620
from infrahub.core.schema import MainSchemaTypes
21+
from infrahub.database import InfrahubDatabase
22+
1723

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

233239
return menu_items
240+
241+
242+
@router.get("/new")
243+
async def get_new_menu(
244+
db: InfrahubDatabase = Depends(get_db),
245+
branch: Branch = Depends(get_branch_dep),
246+
account_session: AccountSession = Depends(get_current_user),
247+
) -> Menu:
248+
log.info("new_menu_request", branch=branch.name)
249+
250+
menu_items = await registry.manager.query(db=db, schema=CoreMenuItem, branch=branch, prefetch_relationships=True)
251+
menu = await generate_menu(db=db, branch=branch, account=account_session, menu_items=menu_items)
252+
253+
return menu.to_rest()

backend/infrahub/core/constants/infrahubkind.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
IPADDRESSPOOL = "CoreIPAddressPool"
3535
IPPREFIX = "BuiltinIPPrefix"
3636
IPPREFIXPOOL = "CoreIPPrefixPool"
37+
MENUITEM = "CoreMenuItem"
3738
NAMESPACE = "IpamNamespace"
3839
NODE = "CoreNode"
3940
NUMBERPOOL = "CoreNumberPool"

backend/infrahub/core/initialization.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@
2020
from infrahub.core.node.resource_manager.ip_address_pool import CoreIPAddressPool
2121
from infrahub.core.node.resource_manager.ip_prefix_pool import CoreIPPrefixPool
2222
from infrahub.core.node.resource_manager.number_pool import CoreNumberPool
23-
from infrahub.core.protocols import CoreAccount
23+
from infrahub.core.protocols import CoreAccount, CoreMenuItem
2424
from infrahub.core.root import Root
2525
from infrahub.core.schema import SchemaRoot, core_models, internal_schema
2626
from infrahub.core.schema.manager import SchemaManager
2727
from infrahub.database import InfrahubDatabase
2828
from infrahub.exceptions import DatabaseError
2929
from infrahub.log import get_logger
30+
from infrahub.menu.menu import default_menu
31+
from infrahub.menu.models import MenuItemDefinition
3032
from infrahub.permissions import PermissionBackend
3133
from infrahub.storage import InfrahubObjectStorage
3234
from infrahub.utils import format_label
@@ -305,6 +307,22 @@ async def create_initial_permission(db: InfrahubDatabase) -> Node:
305307
return permission
306308

307309

310+
async def create_menu_children(db: InfrahubDatabase, parent: CoreMenuItem, children: list[MenuItemDefinition]) -> None:
311+
for child in children:
312+
obj = await child.to_node(db=db, parent=parent)
313+
await obj.save(db=db)
314+
if child.children:
315+
await create_menu_children(db=db, parent=obj, children=child.children)
316+
317+
318+
async def create_default_menu(db: InfrahubDatabase) -> None:
319+
for item in default_menu:
320+
obj = await item.to_node(db=db)
321+
await obj.save(db=db)
322+
if item.children:
323+
await create_menu_children(db=db, parent=obj, children=item.children)
324+
325+
308326
async def create_super_administrator_role(db: InfrahubDatabase) -> Node:
309327
permission = await Node.init(db=db, schema=InfrahubKind.GLOBALPERMISSION)
310328
await permission.new(
@@ -364,6 +382,11 @@ async def first_time_initialization(db: InfrahubDatabase) -> None:
364382
await default_branch.save(db=db)
365383
log.info("Created the Schema in the database", hash=default_branch.active_schema_hash.main)
366384

385+
# --------------------------------------------------
386+
# Create Default Menu
387+
# --------------------------------------------------
388+
await create_default_menu(db=db)
389+
367390
# --------------------------------------------------
368391
# Create Default Users and Groups
369392
# --------------------------------------------------

backend/infrahub/core/manager.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,21 +1043,28 @@ async def get_one(
10431043
node = result[id]
10441044
node_schema = node.get_schema()
10451045

1046+
kind_validation = None
1047+
if kind:
1048+
node_schema_validation = get_schema(db=db, branch=branch, node_schema=kind)
1049+
kind_validation = node_schema_validation.kind
1050+
10461051
# Temporary list of exception to the validation of the kind
10471052
kind_validation_exceptions = [
10481053
("CoreChangeThread", "CoreObjectThread"), # issue/3318
10491054
]
10501055

1051-
if kind and (node_schema.kind != kind and kind not in node_schema.inherit_from):
1056+
if kind_validation and (
1057+
node_schema.kind != kind_validation and kind_validation not in node_schema.inherit_from
1058+
):
10521059
for item in kind_validation_exceptions:
1053-
if item[0] == kind and item[1] == node.get_kind():
1060+
if item[0] == kind_validation and item[1] == node.get_kind():
10541061
return node
10551062

10561063
raise NodeNotFoundError(
10571064
branch_name=branch.name,
1058-
node_type=kind,
1065+
node_type=kind_validation,
10591066
identifier=id,
1060-
message=f"Node with id {id} exists, but it is a {node.get_kind()}, not {kind}",
1067+
message=f"Node with id {id} exists, but it is a {node.get_kind()}, not {kind_validation}",
10611068
)
10621069

10631070
return node

backend/infrahub/core/protocols.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,20 @@ class CoreGroup(CoreNode):
133133
children: RelationshipManager
134134

135135

136+
class CoreMenu(CoreNode):
137+
namespace: String
138+
name: String
139+
label: StringOptional
140+
path: StringOptional
141+
description: StringOptional
142+
icon: StringOptional
143+
protected: Boolean
144+
order_weight: Integer
145+
section: Enum
146+
parent: RelationshipManager
147+
children: RelationshipManager
148+
149+
136150
class CoreProfile(CoreNode):
137151
profile_name: String
138152
profile_priority: IntegerOptional
@@ -365,6 +379,10 @@ class CoreIPPrefixPool(CoreResourcePool, LineageSource):
365379
ip_namespace: RelationshipManager
366380

367381

382+
class CoreMenuItem(CoreMenu):
383+
pass
384+
385+
368386
class CoreNumberPool(CoreResourcePool, LineageSource):
369387
node: String
370388
node_attribute: String

backend/infrahub/core/schema/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from infrahub.core.constants import RESTRICTED_NAMESPACES
99
from infrahub.core.models import HashableModel
10+
from infrahub.exceptions import SchemaNotFoundError
1011

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

5859
return True
5960

61+
def get(self, name: str) -> Union[NodeSchema, GenericSchema]:
62+
"""Check if a schema exist locally as a node or as a generic."""
63+
64+
for item in self.nodes + self.generics:
65+
if item.kind == name:
66+
return item
67+
68+
raise SchemaNotFoundError(branch_name="undefined", identifier=name)
69+
6070
def validate_namespaces(self) -> list[str]:
6171
models = self.nodes + self.generics
6272
errors: list[str] = []

backend/infrahub/core/schema/definitions/core.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from infrahub.core.constants import (
44
DEFAULT_KIND_MAX_LENGTH,
55
DEFAULT_KIND_MIN_LENGTH,
6+
NAMESPACE_REGEX,
67
AccountRole,
78
AccountStatus,
89
AccountType,
@@ -56,6 +57,44 @@
5657
],
5758
}
5859

60+
# -----------------------------------------------
61+
# Menu Items
62+
# -----------------------------------------------
63+
generic_menu_item: dict[str, Any] = {
64+
"name": "Menu",
65+
"namespace": "Core",
66+
"include_in_menu": False,
67+
"description": "Base node for the menu",
68+
"label": "Menu Item",
69+
"hierarchical": True,
70+
"uniqueness_constraints": [["namespace__value", "name__value"]],
71+
"attributes": [
72+
{"name": "namespace", "kind": "Text", "regex": NAMESPACE_REGEX, "order_weight": 1000},
73+
{"name": "name", "kind": "Text", "order_weight": 1000},
74+
{"name": "label", "kind": "Text", "optional": True, "order_weight": 2000},
75+
{"name": "path", "kind": "Text", "optional": True, "order_weight": 2500},
76+
{"name": "description", "kind": "Text", "optional": True, "order_weight": 3000},
77+
{"name": "icon", "kind": "Text", "optional": True, "order_weight": 4000},
78+
{"name": "protected", "kind": "Boolean", "default_value": False, "read_only": True, "order_weight": 5000},
79+
{"name": "order_weight", "kind": "Number", "default_value": 2000, "order_weight": 6000},
80+
{
81+
"name": "section",
82+
"kind": "Text",
83+
"enum": ["object", "internal"],
84+
"default_value": "object",
85+
"order_weight": 7000,
86+
},
87+
],
88+
}
89+
90+
menu_item: dict[str, Any] = {
91+
"name": "MenuItem",
92+
"namespace": "Core",
93+
"include_in_menu": False,
94+
"description": "Menu Item",
95+
"label": "Menu Item",
96+
"inherit_from": ["CoreMenu"],
97+
}
5998

6099
core_models: dict[str, Any] = {
61100
"generics": [
@@ -938,8 +977,10 @@
938977
{"name": "description", "kind": "Text", "optional": True, "order_weight": 3000},
939978
],
940979
},
980+
generic_menu_item,
941981
],
942982
"nodes": [
983+
menu_item,
943984
{
944985
"name": "StandardGroup",
945986
"namespace": "Core",

backend/infrahub/graphql/manager.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
InfrahubIPPrefixMutation,
3131
)
3232
from .mutations.main import InfrahubMutation
33+
from .mutations.menu import InfrahubCoreMenuMutation
3334
from .mutations.proposed_change import InfrahubProposedChangeMutation
3435
from .mutations.repository import InfrahubRepositoryMutation
3536
from .mutations.resource_manager import (
@@ -415,6 +416,7 @@ def generate_mutation_mixin(self) -> type[object]:
415416
InfrahubKind.GRAPHQLQUERY: InfrahubGraphQLQueryMutation,
416417
InfrahubKind.NAMESPACE: InfrahubIPNamespaceMutation,
417418
InfrahubKind.NUMBERPOOL: InfrahubNumberPoolMutation,
419+
InfrahubKind.MENUITEM: InfrahubCoreMenuMutation,
418420
}
419421

420422
if isinstance(node_schema, NodeSchema) and node_schema.is_ip_prefix():

backend/infrahub/graphql/mutations/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ async def mutate_delete(
317317
data: InputObjectType,
318318
branch: Branch,
319319
at: str,
320-
):
320+
) -> tuple[Node, Self]:
321321
context: GraphqlContext = info.context
322322

323323
obj = await NodeManager.find_object(
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
from typing import TYPE_CHECKING, Any, Optional
2+
3+
from graphene import InputObjectType, Mutation
4+
from graphql import GraphQLResolveInfo
5+
from typing_extensions import Self
6+
7+
from infrahub.core.branch import Branch
8+
from infrahub.core.constants import RESTRICTED_NAMESPACES
9+
from infrahub.core.manager import NodeManager
10+
from infrahub.core.node import Node
11+
from infrahub.core.protocols import CoreMenuItem
12+
from infrahub.core.schema import NodeSchema
13+
from infrahub.database import InfrahubDatabase
14+
from infrahub.exceptions import ValidationError
15+
from infrahub.graphql.mutations.main import InfrahubMutationMixin
16+
17+
from .main import InfrahubMutationOptions
18+
19+
if TYPE_CHECKING:
20+
from infrahub.graphql.initialization import GraphqlContext
21+
22+
EXTENDED_RESTRICTED_NAMESPACES = RESTRICTED_NAMESPACES + ["Builtin"]
23+
24+
25+
def validate_namespace(data: InputObjectType) -> None:
26+
namespace = data.get("namespace")
27+
if isinstance(namespace, dict) and "value" in namespace:
28+
namespace_value = str(namespace.get("value"))
29+
if namespace_value in EXTENDED_RESTRICTED_NAMESPACES:
30+
raise ValidationError(
31+
input_value={"namespace": f"{namespace_value} is not valid, it's a restricted namespace"}
32+
)
33+
34+
35+
class InfrahubCoreMenuMutation(InfrahubMutationMixin, Mutation):
36+
@classmethod
37+
def __init_subclass_with_meta__( # pylint: disable=arguments-differ
38+
cls, schema: NodeSchema, _meta: Optional[Any] = None, **options: dict[str, Any]
39+
) -> None:
40+
# Make sure schema is a valid NodeSchema Node Class
41+
if not isinstance(schema, NodeSchema):
42+
raise ValueError(f"You need to pass a valid NodeSchema in '{cls.__name__}.Meta', received '{schema}'")
43+
44+
if not _meta:
45+
_meta = InfrahubMutationOptions(cls)
46+
_meta.schema = schema
47+
48+
super().__init_subclass_with_meta__(_meta=_meta, **options)
49+
50+
@classmethod
51+
async def mutate_create(
52+
cls,
53+
info: GraphQLResolveInfo,
54+
data: InputObjectType,
55+
branch: Branch,
56+
at: str,
57+
database: Optional[InfrahubDatabase] = None,
58+
) -> tuple[Node, Self]:
59+
validate_namespace(data=data)
60+
61+
obj, result = await super().mutate_create(info=info, data=data, branch=branch, at=at)
62+
63+
return obj, result
64+
65+
@classmethod
66+
async def mutate_update(
67+
cls,
68+
info: GraphQLResolveInfo,
69+
data: InputObjectType,
70+
branch: Branch,
71+
at: str,
72+
database: Optional[InfrahubDatabase] = None,
73+
node: Optional[Node] = None,
74+
) -> tuple[Node, Self]:
75+
context: GraphqlContext = info.context
76+
77+
obj = await NodeManager.find_object(
78+
db=context.db, kind=CoreMenuItem, id=data.get("id"), hfid=data.get("hfid"), branch=branch, at=at
79+
)
80+
validate_namespace(data=data)
81+
82+
if obj.protected.value:
83+
raise ValidationError(input_value="This object is protected, it can't be modified.")
84+
85+
obj, result = await super().mutate_update(info=info, data=data, branch=branch, at=at, node=obj) # type: ignore[assignment]
86+
return obj, result # type: ignore[return-value]
87+
88+
@classmethod
89+
async def mutate_delete(
90+
cls,
91+
info: GraphQLResolveInfo,
92+
data: InputObjectType,
93+
branch: Branch,
94+
at: str,
95+
) -> tuple[Node, Self]:
96+
context: GraphqlContext = info.context
97+
obj = await NodeManager.find_object(
98+
db=context.db, kind=CoreMenuItem, id=data.get("id"), hfid=data.get("hfid"), branch=branch, at=at
99+
)
100+
if obj.protected.value:
101+
raise ValidationError(input_value="This object is protected, it can't be deleted.")
102+
103+
return await super().mutate_delete(info=info, data=data, branch=branch, at=at)

0 commit comments

Comments
 (0)