Skip to content

Commit 3fefca3

Browse files
authored
Enable attribute selection in node attribute match form (#6488)
* add constants * add custom form for node trigger rule * use custom form * fix handle submit * 🔧 * update comment * remove log * 🔧 * update types * add custom form for node trigger match * rename const * get correct attributes for dropdown * update type * use new hook * remove log * 🔧 * use new hook * prefill trigger option * fix util function * update test to handle new use case * add const * use NodeKind in attributes for core namespace and node_kind attributes * migrate webhook form * remove forms * remove const * remove unused props * fix default value * fix current value * improve relationship logic * remove logs * type * update types
1 parent 93caf3d commit 3fefca3

File tree

7 files changed

+266
-11
lines changed

7 files changed

+266
-11
lines changed

frontend/app/src/entities/nodes/object-item-details/action-buttons/relationships-buttons.tsx

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { QSP } from "@/config/qsp";
2-
import { AttributeType, Node, RelationshipType } from "@/entities/nodes/getObjectItemDisplayValue";
2+
import { AttributeType, RelationshipType } from "@/entities/nodes/getObjectItemDisplayValue";
33
import { ADD_RELATIONSHIP } from "@/entities/nodes/relationships/api/add-relationships-from-api";
44
import { Permission } from "@/entities/permission/types";
55
import { genericSchemasAtom, nodeSchemasAtom } from "@/entities/schema/stores/schema.atom";
@@ -148,16 +148,12 @@ export function RelationshipsButtons({
148148
onCancel={() => {
149149
setShowAddDrawer(false);
150150
}}
151-
kind={relationshipSchemaData?.peer!}
152151
currentObject={{
153-
device: {
154-
node: {
155-
id: objectid!,
156-
display_label: objectDetailsData.display_label,
157-
__typename: objectDetailsData.__typename,
158-
},
152+
[peerRelationshipSchema.name]: {
153+
node: objectDetailsData,
159154
},
160155
}}
156+
kind={relationshipSchemaData?.peer!}
161157
/>
162158
) : (
163159
<DynamicForm
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
export const NODE_TRIGGER_ATTRIBUTE = "CoreNodeTriggerAttributeMatch";
1+
export const NODE_TRIGGER_ATTRIBUTE_MATCH = "CoreNodeTriggerAttributeMatch";
22
export const NODE_TRIGGER_RELATIONSHIP = "CoreNodeTriggerRelationshipMatch";
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import { createObject } from "@/entities/nodes/api/createObject";
2+
import { updateObjectWithId } from "@/entities/nodes/api/updateObjectWithId";
3+
import graphqlClient from "@/shared/api/graphql/graphqlClientApollo";
4+
import { Button } from "@/shared/components/buttons/button-primitive";
5+
import { NodeFormProps } from "@/shared/components/form/node-form";
6+
import { DynamicDropdownFieldProps, FormFieldValue } from "@/shared/components/form/type";
7+
import { getCurrentFieldValue } from "@/shared/components/form/utils/getFieldDefaultValue";
8+
import { getCreateMutationFromFormDataOnly } from "@/shared/components/form/utils/mutations/getCreateMutationFromFormData";
9+
import { ALERT_TYPES, Alert } from "@/shared/components/ui/alert";
10+
import { Form, FormSubmit } from "@/shared/components/ui/form";
11+
import { datetimeAtom } from "@/shared/stores/time.atom";
12+
import { stringifyWithoutQuotes } from "@/shared/utils/string";
13+
import { gql } from "@apollo/client";
14+
import { useAtomValue } from "jotai";
15+
import { FieldValues, useForm } from "react-hook-form";
16+
import { toast } from "react-toastify";
17+
18+
import { useCurrentBranch } from "@/entities/branches/ui/branches-provider";
19+
import { AttributeType, RelationshipType } from "@/entities/nodes/getObjectItemDisplayValue";
20+
import { useGetObject } from "@/entities/nodes/object/domain/get-object.query";
21+
import { useSchema } from "@/entities/schema/ui/hooks/useSchema";
22+
import { DynamicInput } from "@/shared/components/form/dynamic-form";
23+
import { LabelFormField } from "@/shared/components/form/fields/common";
24+
import DropdownField from "@/shared/components/form/fields/dropdown.field";
25+
import { getFormFieldsFromSchema } from "@/shared/components/form/utils/getFormFieldsFromSchema";
26+
import { getRelationshipDefaultValue } from "@/shared/components/form/utils/getRelationshipDefaultValue";
27+
import { DropdownOption } from "@/shared/components/inputs/dropdown";
28+
import { Skeleton } from "@/shared/components/skeleton";
29+
import { useParams } from "react-router";
30+
import { NODE_TRIGGER_ATTRIBUTE_MATCH } from "../constants";
31+
32+
interface NodeAttributeMatchFormProps extends NodeFormProps {}
33+
34+
export const NodeAttributeMatchForm = ({
35+
currentObject,
36+
objectTemplate,
37+
isUpdate,
38+
onSuccess,
39+
onCancel,
40+
schema,
41+
...props
42+
}: NodeAttributeMatchFormProps) => {
43+
const { currentBranch } = useCurrentBranch();
44+
const date = useAtomValue(datetimeAtom);
45+
const { objectid } = useParams();
46+
47+
const { data, isPending } = useGetObject({ objectSchema: schema, objectId: objectid });
48+
49+
const schemaFields = getFormFieldsFromSchema({
50+
...props,
51+
schema,
52+
initialObject: currentObject,
53+
isUpdate,
54+
});
55+
56+
const attributeField = schemaFields.find((field) => {
57+
return field.name === "attribute_name";
58+
}) as DynamicDropdownFieldProps;
59+
60+
const fields = schemaFields.filter((field) => {
61+
return field.name !== "attribute_name";
62+
});
63+
64+
const defaultValues = {
65+
attribute_name: getCurrentFieldValue("attribute_name", {
66+
attribute_name: currentObject?.attribute_name as AttributeType,
67+
}),
68+
value: getCurrentFieldValue("value", {
69+
value: currentObject?.value as AttributeType,
70+
}),
71+
value_previous: getCurrentFieldValue("value_previous", {
72+
value_previous: currentObject?.value_previous as AttributeType,
73+
}),
74+
value_match: getCurrentFieldValue("value_match", {
75+
value_match: currentObject?.value_match as AttributeType,
76+
}),
77+
member_of_group: getRelationshipDefaultValue({
78+
relationshipData: currentObject?.member_of_group as RelationshipType | undefined,
79+
relationshipName: "member_of_group",
80+
objectTemplate,
81+
}),
82+
trigger: getRelationshipDefaultValue({
83+
relationshipData: currentObject?.trigger as RelationshipType | undefined,
84+
relationshipName: "trigger",
85+
objectTemplate,
86+
}),
87+
};
88+
89+
const form = useForm<FieldValues>({
90+
defaultValues,
91+
});
92+
93+
async function handleSubmit(data: Record<string, FormFieldValue>) {
94+
try {
95+
const newObject = getCreateMutationFromFormDataOnly(data, currentObject);
96+
97+
if (!Object.keys(newObject).length) {
98+
return;
99+
}
100+
101+
const mutationString = currentObject
102+
? updateObjectWithId({
103+
kind: NODE_TRIGGER_ATTRIBUTE_MATCH,
104+
data: stringifyWithoutQuotes({
105+
id: currentObject.id,
106+
...newObject,
107+
}),
108+
})
109+
: createObject({
110+
kind: NODE_TRIGGER_ATTRIBUTE_MATCH,
111+
data: stringifyWithoutQuotes({
112+
...newObject,
113+
}),
114+
});
115+
116+
const mutation = gql`
117+
${mutationString}
118+
`;
119+
120+
const result = await graphqlClient.mutate({
121+
mutation,
122+
context: {
123+
branch: currentBranch.name,
124+
date,
125+
},
126+
});
127+
128+
if (currentObject) {
129+
toast(<Alert type={ALERT_TYPES.SUCCESS} message={"Node attribute match updated!"} />, {
130+
toastId: "alert-success-node-attribute-match-updated",
131+
});
132+
} else {
133+
toast(<Alert type={ALERT_TYPES.SUCCESS} message={"Node attribute match created!"} />, {
134+
toastId: "alert-success-node-attribute-match-created",
135+
});
136+
}
137+
138+
if (onSuccess)
139+
await onSuccess(
140+
result?.data?.[`${NODE_TRIGGER_ATTRIBUTE_MATCH}${currentObject ? "Update" : "Create"}`]
141+
);
142+
} catch (error: unknown) {
143+
console.error("An error occurred while creating the object: ", error);
144+
}
145+
}
146+
147+
return (
148+
<div className={"bg-white flex flex-col flex-1 overflow-auto p-4"}>
149+
<Form form={form} onSubmit={handleSubmit}>
150+
<NodeAttributeField
151+
field={attributeField}
152+
kind={data?.node_kind?.value}
153+
isLoading={isPending}
154+
/>
155+
156+
{fields.map((field) => {
157+
return <DynamicInput key={field.name} {...field} />;
158+
})}
159+
160+
<div className="text-right">
161+
{onCancel && (
162+
<Button variant="outline" className="mr-2" onClick={onCancel}>
163+
Cancel
164+
</Button>
165+
)}
166+
167+
<FormSubmit>Save</FormSubmit>
168+
</div>
169+
</Form>
170+
</div>
171+
);
172+
};
173+
174+
interface NodeAttributeFieldProps {
175+
kind?: string;
176+
isLoading?: boolean;
177+
field?: DynamicDropdownFieldProps;
178+
}
179+
180+
const NodeAttributeField = ({ field, kind, isLoading }: NodeAttributeFieldProps) => {
181+
const { schema } = useSchema(kind);
182+
183+
if (isLoading) {
184+
return (
185+
<div className="space-y-2">
186+
<LabelFormField
187+
label={"Attribute Name"}
188+
required={!!field?.rules?.required}
189+
description={field?.description}
190+
/>
191+
192+
<Skeleton className="h-10 w-full" />
193+
</div>
194+
);
195+
}
196+
197+
const attributeOptions: Array<DropdownOption> =
198+
schema?.attributes?.map((attribute) => {
199+
return {
200+
value: attribute.name,
201+
label: attribute.label ?? attribute.name,
202+
};
203+
}) ?? [];
204+
205+
return <DropdownField {...field} name="attribute_name" items={attributeOptions} />;
206+
};

frontend/app/src/shared/components/form/node-form.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,13 @@ export type NodeFormProps = {
3535
className?: string;
3636
schema: NodeSchema | ProfileSchema;
3737
profiles?: ProfileData[];
38-
onSuccess?: (newObject: any) => void;
3938
currentObject?: Record<string, AttributeType | RelationshipType>;
4039
objectTemplate?: NodeObject | null;
4140
isFilterForm?: boolean;
4241
isUpdate?: boolean;
4342
onSubmit?: (data: NodeFormSubmitParams) => void;
43+
onSuccess?: (newObject: any) => void;
44+
onCancel?: (newObject: any) => void;
4445
};
4546

4647
export const NodeForm = ({

frontend/app/src/shared/components/form/object-form.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import { GlobalPermissionForm } from "@/entities/role-manager/ui/global-permissi
2222
import { ObjectPermissionForm } from "@/entities/role-manager/ui/object-permissions-form";
2323
import { useSchema } from "@/entities/schema/ui/hooks/useSchema";
2424
import { getTemplateRelationshipFromSchema } from "@/entities/schema/utils/get-template-relationship-from-schema";
25+
import { NODE_TRIGGER_ATTRIBUTE_MATCH } from "@/entities/triggers/constants";
26+
import { NodeAttributeMatchForm } from "@/entities/triggers/ui/node-attribute-match-form";
2527
import NoDataFound from "@/shared/components/errors/no-data-found";
2628
import { DynamicFormProps } from "@/shared/components/form/dynamic-form";
2729
import { GenericObjectForm } from "@/shared/components/form/generic-object-form";
@@ -118,6 +120,10 @@ const ObjectForm = ({ kind, currentProfiles, ...props }: ObjectFormProps) => {
118120
return <IpPrefixPoolForm schema={schema} {...props} />;
119121
}
120122

123+
if (kind === NODE_TRIGGER_ATTRIBUTE_MATCH) {
124+
return <NodeAttributeMatchForm schema={schema} {...props} />;
125+
}
126+
121127
if (isGeneric) {
122128
return <GenericObjectForm genericSchema={schema} {...props} />;
123129
}

frontend/app/src/shared/components/form/utils/mutations/getCreateMutationFromFormData.test.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ describe("getCreateMutationFromFormData", () => {
4141
expect(mutationData).to.deep.equal({});
4242
});
4343

44-
it("returns empty if form data if empty", () => {
44+
it("returns empty if form data is empty", () => {
4545
// GIVEN
4646
const fields: Array<DynamicFieldProps> = [buildField()];
4747
const formData: Record<string, FormFieldValue> = {};
@@ -486,4 +486,24 @@ describe("getCreateMutationFromFormDataOnly", () => {
486486
field2: { value: "changed" },
487487
});
488488
});
489+
490+
it("handles relationship value correctly", () => {
491+
// GIVEN
492+
const formData: Record<string, FormFieldValue> = {
493+
field1: {
494+
source: {
495+
type: "user",
496+
},
497+
value: { id: "peer-id", display_label: "peer test", __typename: "PeerKind" },
498+
},
499+
};
500+
501+
// WHEN
502+
const mutationData = getCreateMutationFromFormDataOnly(formData);
503+
504+
// THEN
505+
expect(mutationData).to.deep.equal({
506+
field1: { id: "peer-id" },
507+
});
508+
});
489509
});

frontend/app/src/shared/components/form/utils/mutations/getCreateMutationFromFormData.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,32 @@ export const getCreateMutationFromFormDataOnly = (
7979
if (currentObject && data.value === currentObject[name]?.value) return acc;
8080

8181
if (data.source?.type === "user") {
82+
if (typeof data.value === "object" && data.value !== null) {
83+
if (Array.isArray(data.value)) {
84+
// To differentiate between list (string[]) and relationship (Node[])
85+
if (data.value.every((value) => typeof value === "string")) {
86+
return {
87+
...acc,
88+
[name]: { value: data.value },
89+
};
90+
}
91+
92+
if (data.value.every((value) => "id" in value)) {
93+
return {
94+
...acc,
95+
[name]: data.value.map(({ id }) => ({ id })),
96+
};
97+
}
98+
}
99+
100+
if ("id" in data.value) {
101+
return {
102+
...acc,
103+
[name]: { id: data.value.id },
104+
};
105+
}
106+
}
107+
82108
const fieldValue = data.value === "" ? null : data.value;
83109

84110
return {

0 commit comments

Comments
 (0)