From fc2fef755ae48e4e2fcfd26949e62649c5c8ad76 Mon Sep 17 00:00:00 2001
From: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com>
Date: Thu, 12 Mar 2026 09:58:46 +0100
Subject: [PATCH] fix(ui): query parameters on Attack Paths stuck between
queries (#10306)
---
ui/CHANGELOG.md | 1 +
.../_components/graph/graph-loading.tsx | 21 +--
.../_components/query-parameters-form.tsx | 158 ++++++++----------
.../query-builder/_hooks/use-query-builder.ts | 109 ++++++------
4 files changed, 142 insertions(+), 147 deletions(-)
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 = () => {