diff --git a/backend/infrahub/core/manager.py b/backend/infrahub/core/manager.py index d1e4cfb3b1..31fb943e4f 100644 --- a/backend/infrahub/core/manager.py +++ b/backend/infrahub/core/manager.py @@ -803,8 +803,21 @@ async def get_one_by_hfid( hfid_str = " :: ".join(hfid) - if not node_schema.human_friendly_id or len(node_schema.human_friendly_id) != len(hfid): - raise NodeNotFoundError(branch_name=branch.name, node_type=kind_str, identifier=hfid_str) + if not node_schema.human_friendly_id: + raise NodeNotFoundError( + branch_name=branch.name, + node_type=kind_str, + identifier=hfid_str, + message=f"Unable to lookup node by HFID, schema '{node_schema.kind}' does not have a HFID defined.", + ) + + if len(node_schema.human_friendly_id) != len(hfid): + raise NodeNotFoundError( + branch_name=branch.name, + node_type=kind_str, + identifier=hfid_str, + message=f"Unable to lookup node by HFID, schema '{node_schema.kind}' HFID does not contain the same number of elements as {hfid}", + ) filters = {} for key, item in zip(node_schema.human_friendly_id, hfid, strict=False): diff --git a/backend/infrahub/core/schema/schema_branch.py b/backend/infrahub/core/schema/schema_branch.py index 4d52858320..08b13fb723 100644 --- a/backend/infrahub/core/schema/schema_branch.py +++ b/backend/infrahub/core/schema/schema_branch.py @@ -1909,10 +1909,8 @@ def manage_object_template_relationships(self) -> None: self.set(name=node_name, schema=node_schema) - def add_relationships_to_template(self, node: NodeSchema) -> None: + def add_relationships_to_template(self, node: NodeSchema | GenericSchema) -> None: template_schema = self.get(name=self._get_object_template_kind(node_kind=node.kind), duplicate=False) - if template_schema.is_generic_schema: - return # Remove previous relationships to account for new ones template_schema.relationships = [ @@ -1954,6 +1952,7 @@ def add_relationships_to_template(self, node: NodeSchema) -> None: label=f"{relationship.name} template".title() if relationship.kind in [RelationshipKind.COMPONENT, RelationshipKind.PARENT] else relationship.name.title(), + inherited=relationship.inherited, ) ) @@ -1983,9 +1982,6 @@ def generate_object_template_from_node( need_template_kinds = [n.kind for n in need_templates] if node.is_generic_schema: - # When needing a template for a generic, we generate an empty shell mostly to make sure that schemas (including the GraphQL one) will - # look right. We don't really care about applying inheritance of fields as it was already processed and actual templates will have the - # correct attributes and relationships template = GenericSchema( name=node.kind, namespace="Template", @@ -1994,43 +1990,44 @@ def generate_object_template_from_node( generate_profile=False, branch=node.branch, include_in_menu=False, + display_labels=["template_name__value"], + human_friendly_id=["template_name__value"], + uniqueness_constraints=[["template_name__value"]], attributes=[template_name_attr], ) for used in node.used_by: if used in need_template_kinds: template.used_by.append(self._get_object_template_kind(node_kind=used)) + else: + template = TemplateSchema( + name=node.kind, + namespace="Template", + label=f"Object template {node.label}", + description=f"Object template for {node.kind}", + branch=node.branch, + include_in_menu=False, + display_labels=["template_name__value"], + human_friendly_id=["template_name__value"], + uniqueness_constraints=[["template_name__value"]], + inherit_from=[InfrahubKind.LINEAGESOURCE, InfrahubKind.NODE, core_template_schema.kind], + default_filter="template_name__value", + attributes=[template_name_attr], + relationships=[ + RelationshipSchema( + name="related_nodes", + identifier="node__objecttemplate", + peer=node.kind, + kind=RelationshipKind.TEMPLATE, + cardinality=RelationshipCardinality.MANY, + branch=BranchSupportType.AWARE, + ) + ], + ) - return template - - template = TemplateSchema( - name=node.kind, - namespace="Template", - label=f"Object template {node.label}", - description=f"Object template for {node.kind}", - branch=node.branch, - include_in_menu=False, - display_labels=["template_name__value"], - human_friendly_id=["template_name__value"], - uniqueness_constraints=[["template_name__value"]], - inherit_from=[InfrahubKind.LINEAGESOURCE, InfrahubKind.NODE, core_template_schema.kind], - default_filter="template_name__value", - attributes=[template_name_attr], - relationships=[ - RelationshipSchema( - name="related_nodes", - identifier="node__objecttemplate", - peer=node.kind, - kind=RelationshipKind.TEMPLATE, - cardinality=RelationshipCardinality.MANY, - branch=BranchSupportType.AWARE, - ) - ], - ) - - for inherited in node.inherit_from: - if inherited in need_template_kinds: - template.inherit_from.append(self._get_object_template_kind(node_kind=inherited)) + for inherited in node.inherit_from: + if inherited in need_template_kinds: + template.inherit_from.append(self._get_object_template_kind(node_kind=inherited)) for node_attr in node.attributes: if node_attr.unique or node_attr.read_only: @@ -2038,7 +2035,7 @@ def generate_object_template_from_node( attr = AttributeSchema( optional=node_attr.optional if is_autogenerated_subtemplate else True, - **node_attr.model_dump(exclude=["id", "unique", "optional", "read_only", "inherited"]), + **node_attr.model_dump(exclude=["id", "unique", "optional", "read_only"]), ) template.attributes.append(attr) 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 88e27a8515..4a093080ba 100644 --- a/backend/tests/unit/core/schema_manager/test_manager_schema.py +++ b/backend/tests/unit/core/schema_manager/test_manager_schema.py @@ -3068,6 +3068,18 @@ async def test_manage_object_templates_with_component_relationships(): # Optional value in component template should match original's assert attr.optional == template_attr.optional + # Verify the generic by checking its attributes and relationships + test_interface_template = schema_branch.get(name=f"Template{TestKind.INTERFACE}", duplicate=False) + assert test_interface_template.is_generic_schema + test_interface = schema_branch.get(name=TestKind.INTERFACE, duplicate=False) + assert test_interface.is_generic_schema + for attr in test_interface.attributes: + template_attr = test_interface_template.get_attribute(name=attr.name) + assert attr.optional == template_attr.optional + for rel in test_interface.relationships: + template_rel = test_interface_template.get_relationship(name=rel.name) + assert template_rel.peer == f"Template{rel.peer}" + # Verify when a node is marked as absent ABSENT_VIRTUAL_INTERFACE = copy.deepcopy(DEVICE_SCHEMA) ABSENT_VIRTUAL_INTERFACE.get(name=TestKind.VIRTUAL_INTERFACE).state = HashableModelState.ABSENT diff --git a/backend/tests/unit/core/test_manager_node.py b/backend/tests/unit/core/test_manager_node.py index 307fc10687..8f403359d0 100644 --- a/backend/tests/unit/core/test_manager_node.py +++ b/backend/tests/unit/core/test_manager_node.py @@ -1,3 +1,5 @@ +import copy + import pytest from infrahub_sdk.uuidt import UUIDT @@ -13,6 +15,8 @@ from infrahub.core.timestamp import Timestamp from infrahub.database import InfrahubDatabase from infrahub.exceptions import NodeNotFoundError +from tests.constants import TestKind +from tests.helpers.schema import DEVICE_SCHEMA async def test_get_one_attribute(db: InfrahubDatabase, default_branch: Branch, criticality_schema): @@ -248,6 +252,26 @@ async def test_get_one_by_hfid( await NodeManager.get_one_by_hfid(db=db, hfid=["Not", "Dog"], kind=dog_schema.kind, raise_on_error=True) +async def test_get_by_hfid_with_invalid_hfid(db: InfrahubDatabase, branch: Branch): + schema = copy.deepcopy(DEVICE_SCHEMA) + # Change device schema to add a HFID + schema.nodes[0].human_friendly_id = ["name__value"] + schema.nodes[0].generate_template = False + + registry.schema.register_schema(schema=schema, branch=branch.name) + + device = await Node.init(db=db, schema=TestKind.DEVICE, branch=branch) + await device.new(db=db, name="device-01", manufacturer="Juniper", height=1, weight=6, airflow="Front to rear") + await device.save(db=db) + device_hfid = await device.get_hfid(db=db) + + with pytest.raises(NodeNotFoundError, match=r"does not have a HFID defined"): + await NodeManager.get_one_by_hfid(db=db, branch=branch, kind=TestKind.INTERFACE_HOLDER, hfid=device_hfid) + + with pytest.raises(NodeNotFoundError, match=r"HFID does not contain the same number of elements"): + await NodeManager.get_one_by_hfid(db=db, branch=branch, kind=TestKind.DEVICE, hfid=device_hfid + ["foo"]) + + async def test_get_many(db: InfrahubDatabase, default_branch: Branch, criticality_low, criticality_medium): nodes = await NodeManager.get_many(db=db, ids=[criticality_low.id, criticality_medium.id]) assert len(nodes) == 2 diff --git a/backend/tests/unit/graphql/test_mutation_create.py b/backend/tests/unit/graphql/test_mutation_create.py index f229013d87..0356d68972 100644 --- a/backend/tests/unit/graphql/test_mutation_create.py +++ b/backend/tests/unit/graphql/test_mutation_create.py @@ -1272,6 +1272,64 @@ async def test_create_without_object_template( assert not device_template_node +async def test_create_sub_object_template_by_hfid( + db: InfrahubDatabase, default_branch: Branch, register_core_models_schema: SchemaBranch, branch: Branch +): + registry.schema.register_schema(schema=DEVICE_SCHEMA, branch=branch.name) + + device_template = await Node.init(db=db, schema=f"Template{TestKind.DEVICE}", branch=branch) + await device_template.new( + db=db, template_name="MX204 Router", manufacturer="Juniper", height=1, weight=6, airflow="Front to rear" + ) + await device_template.save(db=db) + device_template_hfid = await device_template.get_hfid(db=db) + + template = await registry.manager.get_one_by_hfid( + db=db, branch=branch, kind=f"Template{TestKind.INTERFACE_HOLDER}", hfid=device_template_hfid + ) + assert device_template.id == template.id + + query = """ + mutation CreateTemplateInterfaceWithHFID($template_name: String!, $device_template_hfid: [String!], $name: String!, $phys_type: String!) { + TemplateTestingPhysicalInterfaceCreate( + data:{ + template_name: {value: $template_name} + device: {hfid: $device_template_hfid} + name: {value: $name} + phys_type: {value: $phys_type} + } + ) { + ok + object { + id + } + } + } + """ + + gql_params = await prepare_graphql_params(db=db, include_subscription=False, branch=branch) + result = await graphql( + schema=gql_params.schema, + source=query, + context_value=gql_params.context, + root_value=None, + variable_values={ + "template_name": "MX204 et-0/0/0", + "device_template_hfid": device_template_hfid, + "name": "et-0/0/0", + "phys_type": "QSFP28 (100GE)", + }, + ) + assert not result.errors + + node_id = result.data["TemplateTestingPhysicalInterfaceCreate"]["object"]["id"] + assert node_id + + interface_template = await registry.manager.get_one(db=db, branch=branch, id=node_id) + assert interface_template + assert (await interface_template.device.get_peer(db=db)).id == device_template.id + + # These tests have been moved at the end of the file to avoid colliding with other and breaking them diff --git a/changelog/+properhfiderrormessage.changed.md b/changelog/+properhfiderrormessage.changed.md new file mode 100644 index 0000000000..0f7fb7d6b6 --- /dev/null +++ b/changelog/+properhfiderrormessage.changed.md @@ -0,0 +1 @@ +Raise more accurate error when trying to lookup a node by HFID when the schema does not have a HFID or the number of elements does not match \ No newline at end of file diff --git a/changelog/+webhook.added.md b/changelog/+webhook.added.md new file mode 100644 index 0000000000..91a9f10a78 --- /dev/null +++ b/changelog/+webhook.added.md @@ -0,0 +1 @@ +Enable node select in the webhook form to quickly choose the node kind \ No newline at end of file diff --git a/changelog/6287.fixed.md b/changelog/6287.fixed.md new file mode 100644 index 0000000000..e8cd6d3aef --- /dev/null +++ b/changelog/6287.fixed.md @@ -0,0 +1 @@ +Add attributes and relationships to generic templates to ensure proper GraphQL schema generation \ No newline at end of file diff --git a/changelog/6301.fixed.md b/changelog/6301.fixed.md new file mode 100644 index 0000000000..638c378cf0 --- /dev/null +++ b/changelog/6301.fixed.md @@ -0,0 +1 @@ +Fix node lookup by its HFID with a generic template kind \ No newline at end of file diff --git a/docs/docs/guides/computed-attributes.mdx b/docs/docs/guides/computed-attributes.mdx index 569ffeb104..2102a42049 100644 --- a/docs/docs/guides/computed-attributes.mdx +++ b/docs/docs/guides/computed-attributes.mdx @@ -66,9 +66,9 @@ nodes: :::note -In your template, you can utilize most of the built-in filters provided by Jinja2! +In your template, you can utilize most of the **filters** provided by **Jinja2** and **Netutils**! -For more information, please consult the [Schema Reference](../reference). +For more information, please consult the [SDK Templating Reference]($(base_url)python-sdk/reference/templating). ::: diff --git a/docs/docs/guides/jinja2-transform.mdx b/docs/docs/guides/jinja2-transform.mdx index e04bda1272..736182f2a4 100644 --- a/docs/docs/guides/jinja2-transform.mdx +++ b/docs/docs/guides/jinja2-transform.mdx @@ -180,6 +180,14 @@ end {% endif %} ``` +:::note + +In your template, you can utilize most of the **filters** provided by **Jinja2** and **Netutils**! + +For more information, please consult the [SDK Templating Reference]($(base_url)python-sdk/reference/templating). + +::: + ## 4. Create a .infrahub.yml file In the .infrahub.yml file you define what transforms you have in your repository that you want to make available for Infrahub. diff --git a/frontend/app/src/config/constants.tsx b/frontend/app/src/config/constants.tsx index 3598eb062d..224e992b86 100644 --- a/frontend/app/src/config/constants.tsx +++ b/frontend/app/src/config/constants.tsx @@ -57,6 +57,9 @@ export const NUMBER_POOL_OBJECT = "CoreNumberPool"; export const TASK_OBJECT = "InfrahubTask"; +export const STANDARD_WEBHOOK_OBJECT = "CoreStandardWebhook"; +export const CUSTOM_WEBHOOK_OBJECT = "CoreCustomWebhook"; + export const MENU_EXCLUDELIST = [ "CoreChangeComment", "CoreChangeThread", diff --git a/frontend/app/src/entities/nodes/object/ui/object-table/cells/node-kind-cell.tsx b/frontend/app/src/entities/nodes/object/ui/object-table/cells/node-kind-cell.tsx new file mode 100644 index 0000000000..4734354b44 --- /dev/null +++ b/frontend/app/src/entities/nodes/object/ui/object-table/cells/node-kind-cell.tsx @@ -0,0 +1,14 @@ +import { useSchema } from "@/entities/schema/ui/hooks/useSchema"; +import { Badge } from "@/shared/components/ui/badge"; + +export function NodeKindCell({ kind }: { kind: string }) { + const { schema } = useSchema(kind); + + if (!schema) return "-"; + + return ( +
+ {schema.label} {schema.namespace} +
+ ); +} diff --git a/frontend/app/src/entities/nodes/object/ui/object-table/cells/table-attribute-cell.tsx b/frontend/app/src/entities/nodes/object/ui/object-table/cells/table-attribute-cell.tsx index 7fd9609906..5ab9ba9735 100644 --- a/frontend/app/src/entities/nodes/object/ui/object-table/cells/table-attribute-cell.tsx +++ b/frontend/app/src/entities/nodes/object/ui/object-table/cells/table-attribute-cell.tsx @@ -1,6 +1,7 @@ import { AttributeType } from "@/entities/nodes/getObjectItemDisplayValue"; import { ColorCell } from "@/entities/nodes/object/ui/object-table/cells/color-cell"; import { DropdownCell } from "@/entities/nodes/object/ui/object-table/cells/dropdown-cell"; +import { NodeKindCell } from "@/entities/nodes/object/ui/object-table/cells/node-kind-cell"; import { UrlCell } from "@/entities/nodes/object/ui/object-table/cells/url-cell"; import { ATTRIBUTE_KIND } from "@/entities/schema/constants"; import { AttributeKind, AttributeSchema } from "@/entities/schema/types"; @@ -38,6 +39,10 @@ export function TableAttributeCell({ attributeSchema, attributeData }: TableAttr case ATTRIBUTE_KIND.IP_HOST: case ATTRIBUTE_KIND.IP_NETWORK: case ATTRIBUTE_KIND.TEXTAREA: { + if (attributeSchema.name === "node_kind") { + return ; + } + return {attributeData.value}; } case ATTRIBUTE_KIND.URL: { diff --git a/frontend/app/src/entities/webhook/ui/webhook-form.tsx b/frontend/app/src/entities/webhook/ui/webhook-form.tsx new file mode 100644 index 0000000000..f9267f2c2c --- /dev/null +++ b/frontend/app/src/entities/webhook/ui/webhook-form.tsx @@ -0,0 +1,113 @@ +import { useCurrentBranch } from "@/entities/branches/ui/branches-provider"; +import { createObject } from "@/entities/nodes/api/createObject"; +import { updateObjectWithId } from "@/entities/nodes/api/updateObjectWithId"; +import { IP_PREFIX_POOL } from "@/entities/resource-manager/constants"; +import { useSchema } from "@/entities/schema/ui/hooks/useSchema"; +import graphqlClient from "@/shared/api/graphql/graphqlClientApollo"; +import DynamicForm from "@/shared/components/form/dynamic-form"; +import { NodeFormProps } from "@/shared/components/form/node-form"; +import { FormFieldValue } from "@/shared/components/form/type"; +import { getFormFieldsFromSchema } from "@/shared/components/form/utils/getFormFieldsFromSchema"; +import { getCreateMutationFromFormData } from "@/shared/components/form/utils/mutations/getCreateMutationFromFormData"; +import { ALERT_TYPES, Alert } from "@/shared/components/ui/alert"; +import { stringifyWithoutQuotes } from "@/shared/utils/string"; +import { gql } from "@apollo/client"; +import { useMemo } from "react"; +import { toast } from "react-toastify"; + +export interface WebhookFormProps extends NodeFormProps { + kind: string; +} + +export function WebhookForm({ + kind, + currentObject, + isUpdate, + onSuccess, + onSubmit, + ...props +}: WebhookFormProps) { + const { schema: genericPrefixSchema } = useSchema(kind); + const { currentBranch } = useCurrentBranch(); + + const fields = useMemo(() => { + const schemaFields = getFormFieldsFromSchema({ + ...props, + initialObject: currentObject, + isUpdate, + }); + + // Replace default_prefix_type (text) field with a select + return schemaFields.map((field) => { + if (field.name === "node_kind") { + return { + ...field, + type: "kind", + }; + } + + return field; + }); + }, [props, genericPrefixSchema, currentObject, isUpdate]); + + async function handleSubmit(data: Record) { + try { + const newObject = getCreateMutationFromFormData(fields, data); + + if (!Object.keys(newObject).length) { + return; + } + + const mutationString = + isUpdate && currentObject + ? updateObjectWithId({ + kind, + data: stringifyWithoutQuotes({ + id: currentObject.id, + ...newObject, + }), + }) + : createObject({ + kind, + data: stringifyWithoutQuotes(newObject), + }); + + const mutation = gql` + ${mutationString} + `; + + const result = await graphqlClient.mutate({ + mutation, + context: { + branch: currentBranch.name, + }, + }); + + const operationType = isUpdate ? "Update" : "Create"; + const successMessage = isUpdate ? "Webhook updated" : "Webhook created"; + toast(, { + toastId: `alert-success-webhook-${operationType}`, + }); + + if (onSuccess) { + const resultData = result?.data?.[`${IP_PREFIX_POOL}${operationType}`]; + await onSuccess(resultData); + } + } catch (error: unknown) { + console.error( + `An error occurred while ${isUpdate ? "updating" : "creating"} the Webhook:`, + error + ); + } + } + + return ( + ) => + onSubmit ? onSubmit({ formData, fields }) : handleSubmit(formData) + } + className="p-4 overflow-auto" + /> + ); +} diff --git a/frontend/app/src/shared/components/form/dynamic-form.tsx b/frontend/app/src/shared/components/form/dynamic-form.tsx index e3d79cf64c..eb8dcb3c65 100644 --- a/frontend/app/src/shared/components/form/dynamic-form.tsx +++ b/frontend/app/src/shared/components/form/dynamic-form.tsx @@ -21,6 +21,7 @@ import { DynamicFieldProps, FormFieldValue } from "@/shared/components/form/type import { Form, FormProps, FormRef, FormSubmit } from "@/shared/components/ui/form"; import { warnUnexpectedType } from "@/shared/utils/common"; import { forwardRef } from "react"; +import { NodeKindField } from "./fields/node-kind.field"; export interface DynamicFormProps extends Omit { fields: Array; @@ -131,6 +132,9 @@ export const DynamicInput = (props: DynamicFieldProps) => { return ; } + case "kind": { + return ; + } default: { warnUnexpectedType(props); return null; diff --git a/frontend/app/src/shared/components/form/fields/node-kind.field.tsx b/frontend/app/src/shared/components/form/fields/node-kind.field.tsx new file mode 100644 index 0000000000..faca8ebf04 --- /dev/null +++ b/frontend/app/src/shared/components/form/fields/node-kind.field.tsx @@ -0,0 +1,82 @@ +import { nodeSchemasAtom } from "@/entities/schema/stores/schema.atom"; +import { useAtomValue } from "jotai"; +import { useState } from "react"; +import { Badge } from "../../ui/badge"; +import { + Combobox, + ComboboxContent, + ComboboxItem, + ComboboxList, + ComboboxTrigger, +} from "../../ui/combobox"; +import { FormField, FormInput, FormMessage } from "../../ui/form"; +import { DEFAULT_FORM_FIELD_VALUE } from "../constants"; +import { FormAttributeValue, FormFieldProps } from "../type"; +import { updateFormFieldValue } from "../utils/updateFormFieldValue"; +import { LabelFormField } from "./common"; + +export function NodeKindField({ label, description, rules, ...props }: FormFieldProps) { + const nodes = useAtomValue(nodeSchemasAtom); + + return ( + { + const [open, setOpen] = useState(false); + + const fieldData: FormAttributeValue = field.value; + const currentValue = (fieldData?.value as string | undefined) ?? null; + const currentNode = nodes.find((node) => { + return node.kind === currentValue; + }); + + return ( +
+ + + + + + {currentNode && ( +
+ {currentNode?.label} {currentNode?.namespace} +
+ )} +
+
+ + + + {nodes.map((node) => { + return ( + { + const newValue = node.kind === currentValue ? null : node.kind; + field.onChange( + updateFormFieldValue(newValue ?? null, DEFAULT_FORM_FIELD_VALUE) + ); + + setOpen(false); + }} + > +
+ {node.label} {node.namespace} +
+
+ ); + })} +
+
+
+ + +
+ ); + }} + {...props} + /> + ); +} diff --git a/frontend/app/src/shared/components/form/object-form.tsx b/frontend/app/src/shared/components/form/object-form.tsx index 1c62a4633e..cd83798540 100644 --- a/frontend/app/src/shared/components/form/object-form.tsx +++ b/frontend/app/src/shared/components/form/object-form.tsx @@ -2,11 +2,13 @@ import { ACCOUNT_GROUP_OBJECT, ACCOUNT_OBJECT, ACCOUNT_ROLE_OBJECT, + CUSTOM_WEBHOOK_OBJECT, GLOBAL_PERMISSION_OBJECT, NUMBER_POOL_OBJECT, OBJECT_PERMISSION_OBJECT, READONLY_REPOSITORY_KIND, REPOSITORY_KIND, + STANDARD_WEBHOOK_OBJECT, } from "@/config/constants"; import { AttributeType, RelationshipType } from "@/entities/nodes/getObjectItemDisplayValue"; @@ -22,6 +24,7 @@ import { GlobalPermissionForm } from "@/entities/role-manager/ui/global-permissi import { ObjectPermissionForm } from "@/entities/role-manager/ui/object-permissions-form"; import { useSchema } from "@/entities/schema/ui/hooks/useSchema"; import { getTemplateRelationshipFromSchema } from "@/entities/schema/utils/get-template-relationship-from-schema"; +import { WebhookForm } from "@/entities/webhook/ui/webhook-form"; import NoDataFound from "@/shared/components/errors/no-data-found"; import { DynamicFormProps } from "@/shared/components/form/dynamic-form"; import { GenericObjectForm } from "@/shared/components/form/generic-object-form"; @@ -126,6 +129,10 @@ const ObjectForm = ({ kind, currentProfiles, ...props }: ObjectFormProps) => { return ; } + if (kind === STANDARD_WEBHOOK_OBJECT || kind === CUSTOM_WEBHOOK_OBJECT) { + return ; + } + return ; }; diff --git a/frontend/app/src/shared/components/form/type.ts b/frontend/app/src/shared/components/form/type.ts index acab001d9c..11afbc9bbc 100644 --- a/frontend/app/src/shared/components/form/type.ts +++ b/frontend/app/src/shared/components/form/type.ts @@ -159,12 +159,17 @@ export type DynamicSelectFieldProps = FormFieldProps & { items: Array<{ key: string; label: string }>; }; +export type DynamicKindFieldProps = FormFieldProps & { + type: "kind"; +}; + export type DynamicAttributeFieldProps = | DynamicInputFieldProps | DynamicNumberFieldProps | DynamicDropdownFieldProps | DynamicEnumFieldProps - | DynamicSelectFieldProps; + | DynamicSelectFieldProps + | DynamicKindFieldProps; export type DynamicRelationshipFieldProps = Omit & { type: "relationship"; diff --git a/frontend/app/tests/e2e/webhook/webhook.spec.ts b/frontend/app/tests/e2e/webhook/webhook.spec.ts index c5875d6aea..fa257ffbb9 100644 --- a/frontend/app/tests/e2e/webhook/webhook.spec.ts +++ b/frontend/app/tests/e2e/webhook/webhook.spec.ts @@ -2,7 +2,7 @@ import { expect, test } from "@playwright/test"; import { ACCOUNT_STATE_PATH } from "../../constants"; import { saveScreenshotForDocs } from "../../utils"; -test.describe.fixme("/objects/CoreWebhook", () => { +test.describe("/objects/CoreWebhook", () => { test.describe("when logged in as admin account", () => { test.describe.configure({ mode: "serial" }); test.use({ storageState: ACCOUNT_STATE_PATH.ADMIN }); @@ -26,6 +26,9 @@ test.describe.fixme("/objects/CoreWebhook", () => { await page.getByLabel("Branch Scope").click(); await page.getByRole("option", { name: "All Branches All branches" }).click(); + await page.getByRole("combobox", { name: "Node Kind" }).click(); + await page.getByRole("option", { name: "Account Core" }).click(); + await page.getByLabel("Description").fill("Ansible EDA Webhook Reciever"); await page.getByLabel("Url *").fill("http://ansible-eda:8080"); @@ -37,7 +40,7 @@ test.describe.fixme("/objects/CoreWebhook", () => { await saveScreenshotForDocs(page, "webhook_create"); await page.getByRole("button", { name: "Save" }).click(); - await expect(page.getByText("StandardWebhook created")).toBeVisible(); + await expect(page.getByText("Webhook created")).toBeVisible(); }); }); @@ -64,5 +67,23 @@ test.describe.fixme("/objects/CoreWebhook", () => { await saveScreenshotForDocs(page, "webhook_detail"); }); }); + + test("Delete webhook", async ({ page }) => { + await test.step("load webhooks", async () => { + await page.goto("/objects/CoreWebhook"); + await expect(page.getByTestId("object-header")).toContainText("Webhook"); + }); + + await test.step("access and delete webhook", async () => { + await page.getByRole("link", { name: "Ansible EDA" }).click(); + await expect( + page.getByTestId("object-header").getByText("Ansible EDA", { exact: true }) + ).toBeVisible(); + await page.getByTestId("delete-button").click(); + await page.getByTestId("modal-delete-confirm").click(); + await expect(page.getByText("Object Ansible EDA deleted")).toBeVisible(); + await page.getByText("No Standard Webhook found").click(); + }); + }); }); });