|
1 | | -import { NUMBER_POOL_OBJECT } from "@/config/constants"; |
2 | | -import { useAuth } from "@/entities/authentication/ui/useAuth"; |
3 | | -import { currentBranchAtom } from "@/entities/branches/stores"; |
| 1 | +import { useCurrentBranch } from "@/entities/branches/ui/branches-provider"; |
4 | 2 | import { IP_ADDRESS_GENERIC } from "@/entities/ipam/constants"; |
5 | 3 | import { createObject } from "@/entities/nodes/api/createObject"; |
6 | 4 | import { updateObjectWithId } from "@/entities/nodes/api/updateObjectWithId"; |
7 | | -import { AttributeType, RelationshipType } from "@/entities/nodes/getObjectItemDisplayValue"; |
8 | | -import { nodeSchemasAtom } from "@/entities/schema/stores/schema.atom"; |
9 | | -import { schemaKindLabelState } from "@/entities/schema/stores/schemaKindLabel.atom"; |
| 5 | +import { getSchema } from "@/entities/schema/domain/get-schema"; |
10 | 6 | import { useSchema } from "@/entities/schema/ui/hooks/useSchema"; |
11 | 7 | import graphqlClient from "@/shared/api/graphql/graphqlClientApollo"; |
12 | | -import { Button } from "@/shared/components/buttons/button-primitive"; |
13 | | -import { DEFAULT_FORM_FIELD_VALUE } from "@/shared/components/form/constants"; |
14 | | -import { LabelFormField } from "@/shared/components/form/fields/common"; |
15 | | -import InputField from "@/shared/components/form/fields/input.field"; |
16 | | -import NumberField from "@/shared/components/form/fields/number.field"; |
17 | | -import RelationshipManyField from "@/shared/components/form/fields/relationship-many.field"; |
18 | | -import RelationshipField from "@/shared/components/form/fields/relationship.field"; |
| 8 | +import DynamicForm from "@/shared/components/form/dynamic-form"; |
19 | 9 | import { NodeFormProps } from "@/shared/components/form/node-form"; |
20 | | -import { FormAttributeValue, FormFieldValue } from "@/shared/components/form/type"; |
21 | | -import { getCurrentFieldValue } from "@/shared/components/form/utils/getFieldDefaultValue"; |
| 10 | +import { DynamicSelectFieldProps, FormFieldValue } from "@/shared/components/form/type"; |
22 | 11 | import { getFormFieldsFromSchema } from "@/shared/components/form/utils/getFormFieldsFromSchema"; |
23 | | -import { getRelationshipDefaultValue } from "@/shared/components/form/utils/getRelationshipDefaultValue"; |
24 | 12 | import { getCreateMutationFromFormData } from "@/shared/components/form/utils/mutations/getCreateMutationFromFormData"; |
25 | | -import { updateFormFieldValue } from "@/shared/components/form/utils/updateFormFieldValue"; |
26 | | -import { isRequired } from "@/shared/components/form/utils/validation"; |
27 | 13 | import { ALERT_TYPES, Alert } from "@/shared/components/ui/alert"; |
28 | | -import { Badge } from "@/shared/components/ui/badge"; |
29 | | -import { |
30 | | - Combobox, |
31 | | - ComboboxContent, |
32 | | - ComboboxItem, |
33 | | - ComboboxList, |
34 | | - ComboboxTrigger, |
35 | | -} from "@/shared/components/ui/combobox"; |
36 | | -import { Form, FormField, FormInput, FormSubmit } from "@/shared/components/ui/form"; |
37 | | -import { datetimeAtom } from "@/shared/stores/time.atom"; |
38 | 14 | import { stringifyWithoutQuotes } from "@/shared/utils/string"; |
39 | 15 | import { gql } from "@apollo/client"; |
40 | | -import { useAtomValue } from "jotai"; |
41 | | -import { useState } from "react"; |
42 | | -import { FieldValues, useForm } from "react-hook-form"; |
| 16 | +import { useMemo } from "react"; |
43 | 17 | import { toast } from "react-toastify"; |
| 18 | +import { capitalize } from "remeda"; |
44 | 19 | import { IP_ADDRESS_POOL } from "../constants"; |
45 | 20 |
|
46 | 21 | const ADDRESS_DEFAULT_TYPE_FIELD_NAME = "default_address_type"; |
47 | 22 |
|
48 | | -interface IpAddressPoolFormProps extends Pick<NodeFormProps, "onSuccess"> { |
49 | | - currentObject?: Record<string, AttributeType | RelationshipType>; |
50 | | - onCancel?: () => void; |
51 | | - onUpdateComplete?: () => void; |
52 | | -} |
| 23 | +interface IpAddressPoolFormProps extends NodeFormProps {} |
53 | 24 |
|
54 | 25 | export const IpAddressPoolForm = ({ |
55 | 26 | currentObject, |
| 27 | + isUpdate, |
| 28 | + onSubmit, |
56 | 29 | onSuccess, |
57 | | - onCancel, |
58 | | - onUpdateComplete, |
| 30 | + ...props |
59 | 31 | }: IpAddressPoolFormProps) => { |
60 | | - const branch = useAtomValue(currentBranchAtom); |
61 | | - const date = useAtomValue(datetimeAtom); |
62 | | - const { schema } = useSchema(IP_ADDRESS_POOL); |
63 | | - const auth = useAuth(); |
| 32 | + const { currentBranch } = useCurrentBranch(); |
| 33 | + const { schema: genericAddressSchema, isGeneric } = useSchema(IP_ADDRESS_GENERIC); |
| 34 | + |
| 35 | + const fields = useMemo(() => { |
| 36 | + const schemaFields = getFormFieldsFromSchema({ |
| 37 | + ...props, |
| 38 | + initialObject: currentObject, |
| 39 | + isUpdate, |
| 40 | + }); |
| 41 | + |
| 42 | + if (!genericAddressSchema || !isGeneric) return schemaFields; |
| 43 | + |
| 44 | + // Replace default_address_type (text) field with a select |
| 45 | + return schemaFields.map((field) => { |
| 46 | + if (field.name === ADDRESS_DEFAULT_TYPE_FIELD_NAME) { |
| 47 | + const items = |
| 48 | + genericAddressSchema.used_by?.map((kind) => { |
| 49 | + const { schema } = getSchema(kind); |
| 50 | + |
| 51 | + if (!schema) { |
| 52 | + return { |
| 53 | + key: kind, |
| 54 | + label: kind, |
| 55 | + }; |
| 56 | + } |
| 57 | + |
| 58 | + return { |
| 59 | + key: kind, |
| 60 | + label: ( |
| 61 | + <div className="flex items-center justify-between w-full"> |
| 62 | + <span>{schema.label}</span> |
| 63 | + <span className="text-xs text-gray-500">{schema.namespace}</span> |
| 64 | + </div> |
| 65 | + ), |
| 66 | + }; |
| 67 | + }) ?? []; |
| 68 | + |
| 69 | + const defaultValue = |
| 70 | + isUpdate && currentObject |
| 71 | + ? field.defaultValue |
| 72 | + : items.length === 1 |
| 73 | + ? { source: { type: "user" }, value: items[0]?.key } |
| 74 | + : field.defaultValue; |
64 | 75 |
|
65 | | - const defaultValues = { |
66 | | - name: getCurrentFieldValue("name", currentObject), |
67 | | - description: getCurrentFieldValue("description", currentObject), |
68 | | - default_prefix_length: getCurrentFieldValue("default_prefix_length", currentObject), |
69 | | - resources: getRelationshipDefaultValue({ |
70 | | - relationshipData: currentObject?.resources, |
71 | | - }), |
72 | | - ip_namespace: getRelationshipDefaultValue({ |
73 | | - relationshipData: currentObject?.ip_namespace, |
74 | | - }), |
75 | | - }; |
76 | | - |
77 | | - const fields = getFormFieldsFromSchema({ |
78 | | - schema, |
79 | | - initialObject: currentObject, |
80 | | - auth, |
81 | | - }); |
82 | | - |
83 | | - const form = useForm<FieldValues>({ |
84 | | - defaultValues, |
85 | | - }); |
86 | | - |
87 | | - const prefixLenghtAttribute = schema?.attributes?.find((attribute) => { |
88 | | - return attribute.name === "default_prefix_length"; |
89 | | - }); |
90 | | - |
91 | | - const resourcesRelatiosnhip = schema?.relationships?.find((relationship) => { |
92 | | - return relationship.name === "resources"; |
93 | | - }); |
94 | | - |
95 | | - const namespaceRelationship = schema?.relationships?.find((relationship) => { |
96 | | - return relationship.name === "ip_namespace"; |
97 | | - }); |
| 76 | + return { |
| 77 | + ...field, |
| 78 | + type: "select", |
| 79 | + items, |
| 80 | + defaultValue, |
| 81 | + } as DynamicSelectFieldProps; |
| 82 | + } |
| 83 | + return field; |
| 84 | + }); |
| 85 | + }, [props.schema.kind, genericAddressSchema?.kind, currentObject, isUpdate]); |
98 | 86 |
|
99 | 87 | async function handleSubmit(data: Record<string, FormFieldValue>) { |
100 | 88 | try { |
@@ -125,156 +113,33 @@ export const IpAddressPoolForm = ({ |
125 | 113 |
|
126 | 114 | const result = await graphqlClient.mutate({ |
127 | 115 | mutation, |
128 | | - context: { |
129 | | - branch: branch?.name, |
130 | | - date, |
131 | | - }, |
| 116 | + context: { branch: currentBranch.name }, |
132 | 117 | }); |
133 | 118 |
|
134 | | - toast(<Alert type={ALERT_TYPES.SUCCESS} message={"Number pool created"} />, { |
135 | | - toastId: "alert-success-number-pool-created", |
| 119 | + const operationType = isUpdate ? "update" : "create"; |
| 120 | + toast(<Alert type={ALERT_TYPES.SUCCESS} message={`IP address pool ${operationType}d`} />, { |
| 121 | + toastId: `alert-success-ip-prefix-pool-${operationType}`, |
136 | 122 | }); |
137 | 123 |
|
138 | | - if (onSuccess) await onSuccess(result?.data?.[`${NUMBER_POOL_OBJECT}Create`]); |
139 | | - if (onUpdateComplete) await onUpdateComplete(); |
| 124 | + if (onSuccess) { |
| 125 | + const resultData = result?.data?.[`${IP_ADDRESS_POOL}${capitalize(operationType)}`]; |
| 126 | + await onSuccess(resultData); |
| 127 | + } |
140 | 128 | } catch (error: unknown) { |
141 | | - console.error("An error occurred while creating the object: ", error); |
| 129 | + console.error( |
| 130 | + `An error occurred while ${isUpdate ? "updating" : "creating"} the IP address pool:`, |
| 131 | + error |
| 132 | + ); |
142 | 133 | } |
143 | 134 | } |
144 | 135 |
|
145 | 136 | return ( |
146 | | - <div className={"bg-custom-white flex flex-col flex-1 overflow-auto p-4"}> |
147 | | - <Form form={form} onSubmit={handleSubmit}> |
148 | | - <InputField name="name" label="Name" rules={{ required: true }} /> |
149 | | - <InputField name="description" label="Description" /> |
150 | | - <AddressTypesCombobox currentObject={currentObject} /> |
151 | | - <NumberField |
152 | | - name="default_prefix_length" |
153 | | - label={prefixLenghtAttribute?.label} |
154 | | - description={prefixLenghtAttribute?.description} |
155 | | - /> |
156 | | - <RelationshipManyField |
157 | | - name="resources" |
158 | | - type="relationship" |
159 | | - label={resourcesRelatiosnhip?.label} |
160 | | - description={resourcesRelatiosnhip?.description} |
161 | | - relationship={{ |
162 | | - name: "resources", |
163 | | - peer: "BuiltinIPPrefix", |
164 | | - kind: "Attribute", |
165 | | - label: "Resources", |
166 | | - }} |
167 | | - rules={{ required: true, validate: { required: isRequired } }} |
168 | | - /> |
169 | | - <RelationshipField |
170 | | - name="ip_namespace" |
171 | | - type="relationship" |
172 | | - label={namespaceRelationship?.label} |
173 | | - description={namespaceRelationship?.description} |
174 | | - relationship={{ peer: "BuiltinIPNamespace", name: "ip_namespace" }} |
175 | | - rules={{ required: true, validate: { required: isRequired } }} |
176 | | - /> |
177 | | - |
178 | | - <div className="text-right"> |
179 | | - {onCancel && ( |
180 | | - <Button variant="outline" className="mr-2" onClick={onCancel}> |
181 | | - Cancel |
182 | | - </Button> |
183 | | - )} |
184 | | - |
185 | | - <FormSubmit>Save</FormSubmit> |
186 | | - </div> |
187 | | - </Form> |
188 | | - </div> |
189 | | - ); |
190 | | -}; |
191 | | - |
192 | | -const AddressTypesCombobox = ({ |
193 | | - currentObject, |
194 | | -}: { currentObject?: Record<string, AttributeType | RelationshipType> }) => { |
195 | | - const { schema } = useSchema(IP_ADDRESS_POOL); |
196 | | - const schemaList = useAtomValue(nodeSchemasAtom); |
197 | | - const { schema: genericSchema, isGeneric } = useSchema(IP_ADDRESS_GENERIC); |
198 | | - const schemaKindName = useAtomValue(schemaKindLabelState); |
199 | | - const [open, setOpen] = useState(false); |
200 | | - |
201 | | - const prefixTypeAttribute = schema?.attributes?.find((attribute) => { |
202 | | - return attribute.name === ADDRESS_DEFAULT_TYPE_FIELD_NAME; |
203 | | - }); |
204 | | - |
205 | | - const items = |
206 | | - (isGeneric && |
207 | | - genericSchema?.used_by?.map((kind) => { |
208 | | - const currentSchema = schemaList.find((schema) => schema.kind === kind); |
209 | | - |
210 | | - return { |
211 | | - value: kind, |
212 | | - label: schemaKindName[kind], |
213 | | - namespace: currentSchema?.namespace, |
214 | | - }; |
215 | | - })) ?? |
216 | | - []; |
217 | | - |
218 | | - const currentValue = getCurrentFieldValue(ADDRESS_DEFAULT_TYPE_FIELD_NAME, currentObject); |
219 | | - |
220 | | - const defaultValue = |
221 | | - (items[0] ?? currentValue) |
222 | | - ? { source: { type: "user" }, value: items[0].value } |
223 | | - : DEFAULT_FORM_FIELD_VALUE; |
224 | | - |
225 | | - return ( |
226 | | - <FormField |
227 | | - name={ADDRESS_DEFAULT_TYPE_FIELD_NAME} |
228 | | - rules={{ required: true, validate: { required: isRequired } }} |
229 | | - defaultValue={defaultValue} |
230 | | - render={({ field }) => { |
231 | | - const fieldData: FormAttributeValue = field.value; |
232 | | - |
233 | | - return ( |
234 | | - <div className="flex flex-col gap-2"> |
235 | | - <LabelFormField |
236 | | - label={prefixTypeAttribute?.label} |
237 | | - required |
238 | | - description={prefixTypeAttribute?.description} |
239 | | - fieldData={fieldData} |
240 | | - /> |
241 | | - |
242 | | - <FormInput> |
243 | | - <Combobox open={open} onOpenChange={setOpen}> |
244 | | - <ComboboxTrigger data-testid="namespace-select"> |
245 | | - {items.find((item) => item.value === fieldData?.value)?.label} |
246 | | - </ComboboxTrigger> |
247 | | - |
248 | | - <ComboboxContent align="start" fitTriggerWidth={false}> |
249 | | - <ComboboxList className="max-w-md"> |
250 | | - {items.map((item) => { |
251 | | - return ( |
252 | | - <ComboboxItem |
253 | | - key={item.value} |
254 | | - value={item.value} |
255 | | - selectedValue={fieldData?.value} |
256 | | - onSelect={() => { |
257 | | - const newValue = fieldData?.value === item.value ? null : item.value; |
258 | | - field.onChange( |
259 | | - updateFormFieldValue(newValue ?? null, DEFAULT_FORM_FIELD_VALUE) |
260 | | - ); |
261 | | - setOpen(false); |
262 | | - }} |
263 | | - > |
264 | | - <div className="overflow-hidden w-full flex items-center justify-between"> |
265 | | - <div className="truncate font-semibold">{item.label}</div> |
266 | | - <Badge className="font-medium">{item.namespace}</Badge> |
267 | | - </div> |
268 | | - </ComboboxItem> |
269 | | - ); |
270 | | - })} |
271 | | - </ComboboxList> |
272 | | - </ComboboxContent> |
273 | | - </Combobox> |
274 | | - </FormInput> |
275 | | - </div> |
276 | | - ); |
277 | | - }} |
| 137 | + <DynamicForm |
| 138 | + fields={fields} |
| 139 | + onSubmit={(formData: Record<string, FormFieldValue>) => |
| 140 | + onSubmit ? onSubmit({ formData, fields }) : handleSubmit(formData) |
| 141 | + } |
| 142 | + className="p-4 overflow-auto" |
278 | 143 | /> |
279 | 144 | ); |
280 | 145 | }; |
0 commit comments