diff --git a/changelog/3489.added.md b/changelog/3489.added.md new file mode 100644 index 0000000000..62a1aeb3c5 --- /dev/null +++ b/changelog/3489.added.md @@ -0,0 +1 @@ +Allow Default Address Type quick selection in the Resource Manager form \ No newline at end of file diff --git a/frontend/app/src/entities/ipam/constants.ts b/frontend/app/src/entities/ipam/constants.ts index 6aa8440ae1..08dc3c39f4 100644 --- a/frontend/app/src/entities/ipam/constants.ts +++ b/frontend/app/src/entities/ipam/constants.ts @@ -1,12 +1,14 @@ import { RELATIONSHIP_VIEW_BLACKLIST } from "@/config/constants"; +import { IP_ADDRESS_POOL, IP_PREFIX_POOL } from "../resource-manager/constants"; export const NAMESPACE_GENERIC = "BuiltinIPNamespace"; export const IP_ADDRESS_GENERIC = "BuiltinIPAddress"; export const IP_PREFIX_GENERIC = "BuiltinIPPrefix"; + export const POOLS_PEER = [IP_ADDRESS_GENERIC, IP_PREFIX_GENERIC]; export const POOLS_DICTIONNARY = { - IpamIPAddress: "CoreIPAddressPool", - IpamIPPrefix: "CoreIPPrefixPool", + IpamIPAddress: IP_ADDRESS_POOL, + IpamIPPrefix: IP_PREFIX_POOL, }; export const TREE_ROOT_ID = "root" as const; diff --git a/frontend/app/src/entities/resource-manager/constants.ts b/frontend/app/src/entities/resource-manager/constants.ts index 6edb4bc960..f6edcb0ece 100644 --- a/frontend/app/src/entities/resource-manager/constants.ts +++ b/frontend/app/src/entities/resource-manager/constants.ts @@ -1,7 +1,8 @@ export const RESOURCE_GENERIC_KIND = "CoreResourcePool"; export const RESOURCE_POOL_UTILIZATION_KIND = "InfrahubResourcePoolUtilization"; export const RESOURCE_POOL_ALLOCATED_KIND = "InfrahubResourcePoolAllocated"; +export const IP_ADDRESS_POOL = "CoreIPAddressPool"; +export const IP_PREFIX_POOL = "CoreIPPrefixPool"; export const NUMBER_POOL_KIND = "CoreNumberPool"; - export const NUMBER_POOL_NODE_FIELD = "node"; export const NUMBER_POOL_NODE_ATTRIBUTE_FIELD = "node_attribute"; diff --git a/frontend/app/src/entities/resource-manager/ui/ip-address-pool-form.tsx b/frontend/app/src/entities/resource-manager/ui/ip-address-pool-form.tsx new file mode 100644 index 0000000000..7eada28454 --- /dev/null +++ b/frontend/app/src/entities/resource-manager/ui/ip-address-pool-form.tsx @@ -0,0 +1,280 @@ +import { NUMBER_POOL_OBJECT } from "@/config/constants"; +import { useAuth } from "@/entities/authentication/ui/useAuth"; +import { currentBranchAtom } from "@/entities/branches/stores"; +import { IP_ADDRESS_GENERIC } from "@/entities/ipam/constants"; +import { createObject } from "@/entities/nodes/api/createObject"; +import { updateObjectWithId } from "@/entities/nodes/api/updateObjectWithId"; +import { AttributeType, RelationshipType } from "@/entities/nodes/getObjectItemDisplayValue"; +import { useSchema } from "@/entities/schema/hooks/useSchema"; +import { schemaState } from "@/entities/schema/stores/schema.atom"; +import { schemaKindLabelState } from "@/entities/schema/stores/schemaKindLabel.atom"; +import graphqlClient from "@/shared/api/graphql/graphqlClientApollo"; +import { Button } from "@/shared/components/buttons/button-primitive"; +import { DEFAULT_FORM_FIELD_VALUE } from "@/shared/components/form/constants"; +import { LabelFormField } from "@/shared/components/form/fields/common"; +import InputField from "@/shared/components/form/fields/input.field"; +import NumberField from "@/shared/components/form/fields/number.field"; +import RelationshipManyField from "@/shared/components/form/fields/relationship-many.field"; +import RelationshipField from "@/shared/components/form/fields/relationship.field"; +import { NodeFormProps } from "@/shared/components/form/node-form"; +import { FormAttributeValue, FormFieldValue } from "@/shared/components/form/type"; +import { getCurrentFieldValue } from "@/shared/components/form/utils/getFieldDefaultValue"; +import { getFormFieldsFromSchema } from "@/shared/components/form/utils/getFormFieldsFromSchema"; +import { getRelationshipDefaultValue } from "@/shared/components/form/utils/getRelationshipDefaultValue"; +import { getCreateMutationFromFormData } from "@/shared/components/form/utils/mutations/getCreateMutationFromFormData"; +import { updateFormFieldValue } from "@/shared/components/form/utils/updateFormFieldValue"; +import { isRequired } from "@/shared/components/form/utils/validation"; +import { ALERT_TYPES, Alert } from "@/shared/components/ui/alert"; +import { Badge } from "@/shared/components/ui/badge"; +import { + Combobox, + ComboboxContent, + ComboboxItem, + ComboboxList, + ComboboxTrigger, +} from "@/shared/components/ui/combobox"; +import { Form, FormField, FormInput, FormSubmit } from "@/shared/components/ui/form"; +import { datetimeAtom } from "@/shared/stores/time.atom"; +import { stringifyWithoutQuotes } from "@/shared/utils/string"; +import { gql } from "@apollo/client"; +import { useAtomValue } from "jotai"; +import { useState } from "react"; +import { FieldValues, useForm } from "react-hook-form"; +import { toast } from "react-toastify"; +import { IP_ADDRESS_POOL } from "../constants"; + +const ADDRESS_DEFAULT_TYPE_FIELD_NAME = "default_address_type"; + +interface IpAddressPoolFormProps extends Pick { + currentObject?: Record; + onCancel?: () => void; + onUpdateComplete?: () => void; +} + +export const IpAddressPoolForm = ({ + currentObject, + onSuccess, + onCancel, + onUpdateComplete, +}: IpAddressPoolFormProps) => { + const branch = useAtomValue(currentBranchAtom); + const date = useAtomValue(datetimeAtom); + const { schema } = useSchema(IP_ADDRESS_POOL); + const auth = useAuth(); + + const defaultValues = { + name: getCurrentFieldValue("name", currentObject), + description: getCurrentFieldValue("description", currentObject), + default_prefix_length: getCurrentFieldValue("default_prefix_length", currentObject), + resources: getRelationshipDefaultValue({ + relationshipData: currentObject?.resources, + }), + ip_namespace: getRelationshipDefaultValue({ + relationshipData: currentObject?.ip_namespace, + }), + }; + + const fields = getFormFieldsFromSchema({ + schema, + initialObject: currentObject, + auth, + }); + + const form = useForm({ + defaultValues, + }); + + const prefixLenghtAttribute = schema?.attributes?.find((attribute) => { + return attribute.name === "default_prefix_length"; + }); + + const resourcesRelatiosnhip = schema?.relationships?.find((relationship) => { + return relationship.name === "resources"; + }); + + const namespaceRelationship = schema?.relationships?.find((relationship) => { + return relationship.name === "ip_namespace"; + }); + + async function handleSubmit(data: Record) { + try { + const newObject = getCreateMutationFromFormData(fields, data); + + if (!Object.keys(newObject).length) { + return; + } + + const mutationString = currentObject + ? updateObjectWithId({ + kind: IP_ADDRESS_POOL, + data: stringifyWithoutQuotes({ + id: currentObject.id, + ...newObject, + }), + }) + : createObject({ + kind: IP_ADDRESS_POOL, + data: stringifyWithoutQuotes({ + ...newObject, + }), + }); + + const mutation = gql` + ${mutationString} + `; + + const result = await graphqlClient.mutate({ + mutation, + context: { + branch: branch?.name, + date, + }, + }); + + toast(, { + toastId: "alert-success-number-pool-created", + }); + + if (onSuccess) await onSuccess(result?.data?.[`${NUMBER_POOL_OBJECT}Create`]); + if (onUpdateComplete) await onUpdateComplete(); + } catch (error: unknown) { + console.error("An error occurred while creating the object: ", error); + } + } + + return ( +
+
+ + + + + + + +
+ {onCancel && ( + + )} + + Save +
+ +
+ ); +}; + +const AddressTypesCombobox = ({ + currentObject, +}: { currentObject?: Record }) => { + const { schema } = useSchema(IP_ADDRESS_POOL); + const schemaList = useAtomValue(schemaState); + const { schema: genericSchema, isGeneric } = useSchema(IP_ADDRESS_GENERIC); + const schemaKindName = useAtomValue(schemaKindLabelState); + const [open, setOpen] = useState(false); + + const prefixTypeAttribute = schema?.attributes?.find((attribute) => { + return attribute.name === ADDRESS_DEFAULT_TYPE_FIELD_NAME; + }); + + const items = + (isGeneric && + genericSchema?.used_by?.map((kind) => { + const currentSchema = schemaList.find((schema) => schema.kind === kind); + + return { + value: kind, + label: schemaKindName[kind], + namespace: currentSchema?.namespace, + }; + })) ?? + []; + + const currentValue = getCurrentFieldValue(ADDRESS_DEFAULT_TYPE_FIELD_NAME, currentObject); + + const defaultValue = + (items[0] ?? currentValue) + ? { source: { type: "user" }, value: items[0].value } + : DEFAULT_FORM_FIELD_VALUE; + + return ( + { + const fieldData: FormAttributeValue = field.value; + + return ( +
+ + + + + + {items.find((item) => item.value === fieldData?.value)?.label} + + + + + {items.map((item) => { + return ( + { + const newValue = fieldData?.value === item.value ? null : item.value; + field.onChange( + updateFormFieldValue(newValue ?? null, DEFAULT_FORM_FIELD_VALUE) + ); + setOpen(false); + }} + > +
+
{item.label}
+ {item.namespace} +
+
+ ); + })} +
+
+
+
+
+ ); + }} + /> + ); +}; diff --git a/frontend/app/src/shared/components/form/dynamic-form.tsx b/frontend/app/src/shared/components/form/dynamic-form.tsx index 841073d9dc..5a08d3c371 100644 --- a/frontend/app/src/shared/components/form/dynamic-form.tsx +++ b/frontend/app/src/shared/components/form/dynamic-form.tsx @@ -114,6 +114,7 @@ export const DynamicInput = (props: DynamicFieldProps) => { } case "relationship": { const { schema: peerSchema } = getSchema(props.relationship.peer); + if (peerSchema && isHierarchicalSchema(peerSchema)) { return ; } diff --git a/frontend/app/src/shared/components/form/object-form.tsx b/frontend/app/src/shared/components/form/object-form.tsx index 9063e75aca..64f846c684 100644 --- a/frontend/app/src/shared/components/form/object-form.tsx +++ b/frontend/app/src/shared/components/form/object-form.tsx @@ -8,7 +8,10 @@ import { READONLY_REPOSITORY_KIND, REPOSITORY_KIND, } from "@/config/constants"; + import { AttributeType, RelationshipType } from "@/entities/nodes/getObjectItemDisplayValue"; +import { IP_ADDRESS_POOL } from "@/entities/resource-manager/constants"; +import { IpAddressPoolForm } from "@/entities/resource-manager/ui/ip-address-pool-form"; import { NumberPoolForm } from "@/entities/resource-manager/ui/number-pool-form"; import { AccountForm } from "@/entities/role-manager/ui/account-form"; import { AccountGroupForm } from "@/entities/role-manager/ui/account-group-form"; @@ -86,6 +89,10 @@ const ObjectForm = ({ kind, currentProfiles, ...props }: ObjectFormProps) => { return ; } + if (kind === IP_ADDRESS_POOL) { + return ; + } + if (isGeneric) { return ; } diff --git a/frontend/app/src/shared/components/ui/command.tsx b/frontend/app/src/shared/components/ui/command.tsx index e267a467b0..61f5222055 100644 --- a/frontend/app/src/shared/components/ui/command.tsx +++ b/frontend/app/src/shared/components/ui/command.tsx @@ -55,7 +55,8 @@ export const CommandItem = React.forwardRef<