diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 564b77e6a9..67dcc83a39 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -22,6 +22,7 @@ All notable changes to the **Prowler UI** are documented in this file. ### 🐞 Fixed - Provider wizard now closes after updating credentials instead of incorrectly advancing to the Launch Scan step, which caused API errors for providers with existing scheduled scans [(#10278)](https://github.com/prowler-cloud/prowler/pull/10278) +- Attack Paths query builder sending stale parameters from previous query selections due to validation schema and default values being recreated on every render [(#10306)](https://github.com/prowler-cloud/prowler/pull/10306) --- diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/graph-loading.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/graph-loading.tsx index cf56231a05..be4bd1b528 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/graph-loading.tsx +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/graph-loading.tsx @@ -1,6 +1,6 @@ "use client"; -import { Skeleton } from "@/components/shadcn/skeleton/skeleton"; +import { TreeSpinner } from "@/components/shadcn/tree-view/tree-spinner"; /** * Loading skeleton for graph visualization @@ -8,17 +8,14 @@ import { Skeleton } from "@/components/shadcn/skeleton/skeleton"; */ export const GraphLoading = () => { return ( -
-
-
- - - -
-

- Loading Attack Paths graph... -

-
+
+ +

+ Loading Attack Paths graph... +

); }; diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-parameters-form.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-parameters-form.tsx index ccbcf60547..24937c0b7b 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-parameters-form.tsx +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-parameters-form.tsx @@ -2,6 +2,7 @@ import { Controller, useFormContext } from "react-hook-form"; +import { Input } from "@/components/shadcn"; import type { AttackPathQuery } from "@/types/attack-paths"; interface QueryParametersFormProps { @@ -21,14 +22,7 @@ export const QueryParametersForm = ({ } = useFormContext(); if (!selectedQuery || !selectedQuery.attributes.parameters.length) { - return ( -
-

- This query requires no parameters. Click "Execute Query" to - proceed. -

-
- ); + return null; } return ( @@ -37,86 +31,82 @@ export const QueryParametersForm = ({ Query Parameters - {selectedQuery.attributes.parameters.map((param) => ( - { - if (param.data_type === "boolean") { - return ( -
-
); }; diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_hooks/use-query-builder.ts b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_hooks/use-query-builder.ts index b05c9d463c..da93b049b9 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_hooks/use-query-builder.ts +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_hooks/use-query-builder.ts @@ -7,6 +7,38 @@ import { z } from "zod"; import type { AttackPathQuery } from "@/types/attack-paths"; +const getValidationSchema = (query?: AttackPathQuery) => { + const schemaObject: Record = {}; + + query?.attributes.parameters.forEach((param) => { + let fieldSchema: z.ZodTypeAny = z + .string() + .min(1, `${param.label} is required`); + + if (param.data_type === "number") { + fieldSchema = z.coerce.number().refine((val) => val >= 0, { + message: `${param.label} must be a non-negative number`, + }); + } else if (param.data_type === "boolean") { + fieldSchema = z.boolean().default(false); + } + + schemaObject[param.name] = fieldSchema; + }); + + return z.object(schemaObject); +}; + +const getDefaultValues = (query?: AttackPathQuery) => { + const defaults: Record = {}; + + query?.attributes.parameters.forEach((param) => { + defaults[param.name] = param.data_type === "boolean" ? false : ""; + }); + + return defaults; +}; + /** * Custom hook for managing query builder form state * Handles query selection, parameter validation, and form submission @@ -14,72 +46,47 @@ import type { AttackPathQuery } from "@/types/attack-paths"; export const useQueryBuilder = (availableQueries: AttackPathQuery[]) => { const [selectedQuery, setSelectedQuery] = useState(null); - // Generate dynamic Zod schema based on selected query parameters - const getValidationSchema = (queryId: string | null) => { - const schemaObject: Record = {}; - - if (queryId) { - const query = availableQueries.find((q) => q.id === queryId); - - if (query) { - query.attributes.parameters.forEach((param) => { - let fieldSchema: z.ZodTypeAny = z - .string() - .min(1, `${param.label} is required`); - - if (param.data_type === "number") { - fieldSchema = z.coerce.number().refine((val) => val >= 0, { - message: `${param.label} must be a non-negative number`, - }); - } else if (param.data_type === "boolean") { - fieldSchema = z.boolean().default(false); - } - - schemaObject[param.name] = fieldSchema; - }); - } - } - - return z.object(schemaObject); - }; - - const getDefaultValues = (queryId: string | null) => { - const defaults: Record = {}; - - const query = availableQueries.find((q) => q.id === queryId); - if (query) { - query.attributes.parameters.forEach((param) => { - defaults[param.name] = param.data_type === "boolean" ? false : ""; - }); - } - - return defaults; - }; + const getQueryById = (queryId: string | null) => + availableQueries.find((query) => query.id === queryId); + const selectedQueryData = getQueryById(selectedQuery); const form = useForm({ - resolver: zodResolver(getValidationSchema(selectedQuery)), + resolver: zodResolver(getValidationSchema(selectedQueryData)), mode: "onChange", - defaultValues: getDefaultValues(selectedQuery), + defaultValues: getDefaultValues(selectedQueryData), + shouldUnregister: true, }); // Update form when selectedQuery changes useEffect(() => { - form.reset(getDefaultValues(selectedQuery), { + form.reset(getDefaultValues(selectedQueryData), { keepDirtyValues: false, }); - }, [selectedQuery]); // eslint-disable-line react-hooks/exhaustive-deps - - const selectedQueryData = availableQueries.find( - (q) => q.id === selectedQuery, - ); + }, [form, selectedQueryData]); const handleQueryChange = (queryId: string) => { setSelectedQuery(queryId); - form.reset(); }; const getQueryParameters = () => { - return form.getValues(); + if (!selectedQueryData?.attributes.parameters.length) { + return undefined; + } + + const values = form.getValues() as Record< + string, + string | number | boolean + >; + + return selectedQueryData.attributes.parameters.reduce< + Record + >((parameters, parameter) => { + const value = values[parameter.name]; + if (value !== undefined) { + parameters[parameter.name] = value; + } + return parameters; + }, {}); }; const isFormValid = () => {