Files
prowler/ui/actions/manage-groups/manage-groups.ts
T
Pablo Fernandez Guerra (PFE) 5b9824c379 feat(ui): filter by provider group across main views (#11659)
Co-authored-by: Pablo F.G <pablo.fernandez@prowler.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 15:32:00 +02:00

289 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { apiBaseUrl, getAuthHeaders, getErrorMessage } from "@/lib";
import { handleApiError, handleApiResponse } from "@/lib/server-actions-helper";
import { ManageGroupPayload, ProviderGroupsResponse } from "@/types/components";
export const getProviderGroups = async ({
page = 1,
query = "",
sort = "",
filters = {},
pageSize = 10,
}: {
page?: number;
query?: string;
sort?: string;
filters?: Record<string, string | number>;
pageSize?: number;
}): Promise<ProviderGroupsResponse | undefined> => {
const headers = await getAuthHeaders({ contentType: false });
if (isNaN(Number(page)) || page < 1)
redirect("/providers?tab=provider-groups");
const url = new URL(`${apiBaseUrl}/provider-groups`);
if (page) url.searchParams.append("page[number]", page.toString());
if (pageSize) url.searchParams.append("page[size]", pageSize.toString());
if (query) url.searchParams.append("filter[search]", query);
if (sort) url.searchParams.append("sort", sort);
// Handle multiple filters
Object.entries(filters).forEach(([key, value]) => {
if (key !== "filter[search]") {
url.searchParams.append(key, String(value));
}
});
try {
const response = await fetch(url.toString(), {
headers,
});
return await handleApiResponse(response);
} catch (error) {
console.error("Error fetching provider groups:", error);
return undefined;
}
};
/**
* Fetches all provider groups by iterating through every page.
* Used to populate filter dropdowns (e.g. the Provider Group selector) without
* the pagination cap that `getProviderGroups` applies for the management table.
*/
export const getAllProviderGroups = async (): Promise<
ProviderGroupsResponse | undefined
> => {
const pageSize = 100; // Larger page size to minimize API calls
const maxPages = 50; // Safety limit: 50 pages × 100 = 5000 groups max
let currentPage = 1;
const allGroups: ProviderGroupsResponse["data"] = [];
let lastResponse: ProviderGroupsResponse | undefined;
let hasMorePages = true;
try {
const headers = await getAuthHeaders({ contentType: false });
while (hasMorePages && currentPage <= maxPages) {
const url = new URL(`${apiBaseUrl}/provider-groups`);
url.searchParams.append("page[number]", currentPage.toString());
url.searchParams.append("page[size]", pageSize.toString());
const response = await fetch(url.toString(), { headers });
const data = (await handleApiResponse(response)) as
| ProviderGroupsResponse
| { error: string; status?: number }
| undefined;
// A later page resolving to an API error payload must abort rather than
// be treated as "no more pages", which would silently truncate groups.
if (data && "error" in data) {
console.error("Error fetching all provider groups:", data.error);
return undefined;
}
if (!data?.data || data.data.length === 0) {
hasMorePages = false;
continue;
}
allGroups.push(...data.data);
lastResponse = data;
const totalPages = data.meta?.pagination?.pages || 1;
if (currentPage >= totalPages) {
hasMorePages = false;
} else {
currentPage++;
}
}
if (hasMorePages && currentPage > maxPages) {
console.error(
`Error fetching all provider groups: exceeded max page limit (${maxPages})`,
);
return undefined;
}
if (lastResponse) {
return {
...lastResponse,
data: allGroups,
meta: {
...lastResponse.meta,
pagination: {
...lastResponse.meta?.pagination,
page: 1,
pages: 1,
count: allGroups.length,
},
},
};
}
return undefined;
} catch (error) {
console.error("Error fetching all provider groups:", error);
return undefined;
}
};
export const getProviderGroupInfoById = async (providerGroupId: string) => {
const headers = await getAuthHeaders({ contentType: false });
const url = new URL(`${apiBaseUrl}/provider-groups/${providerGroupId}`);
try {
const response = await fetch(url.toString(), {
method: "GET",
headers,
});
return await handleApiResponse(response);
} catch (error) {
handleApiError(error);
}
};
export const createProviderGroup = async (formData: FormData) => {
const headers = await getAuthHeaders({ contentType: true });
const name = formData.get("name") as string;
const providersJson = formData.get("providers") as string;
const rolesJson = formData.get("roles") as string;
// Parse JSON strings and handle empty cases
const providers = providersJson ? JSON.parse(providersJson) : [];
const roles = rolesJson ? JSON.parse(rolesJson) : [];
// Prepare base payload
const payload: any = {
data: {
type: "provider-groups",
attributes: {
name,
},
relationships: {},
},
};
// Add relationships only if there are items
if (providers.length > 0) {
payload.data.relationships.providers = {
data: providers,
};
}
if (roles.length > 0) {
payload.data.relationships.roles = {
data: roles,
};
}
const body = JSON.stringify(payload);
try {
const url = new URL(`${apiBaseUrl}/provider-groups`);
const response = await fetch(url.toString(), {
method: "POST",
headers,
body,
});
return await handleApiResponse(response, "/providers?tab=provider-groups");
} catch (error) {
handleApiError(error);
}
};
export const updateProviderGroup = async (
providerGroupId: string,
formData: FormData,
) => {
const headers = await getAuthHeaders({ contentType: true });
const name = formData.get("name") as string;
const providersJson = formData.get("providers") as string;
const rolesJson = formData.get("roles") as string;
const providers = providersJson ? JSON.parse(providersJson) : null;
const roles = rolesJson ? JSON.parse(rolesJson) : null;
const payload: Partial<ManageGroupPayload> = {
data: {
type: "provider-groups",
id: providerGroupId,
attributes: name ? { name } : undefined,
relationships: {},
},
};
// Add relationships only if changes are detected
if (providers) {
payload.data!.relationships!.providers = { data: providers };
}
if (roles) {
payload.data!.relationships!.roles = { data: roles };
}
try {
const url = `${apiBaseUrl}/provider-groups/${providerGroupId}`;
const response = await fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(payload),
});
return await handleApiResponse(response);
} catch (error) {
handleApiError(error);
}
};
export const deleteProviderGroup = async (formData: FormData) => {
const headers = await getAuthHeaders({ contentType: false });
const providerGroupId = formData.get("id");
if (!providerGroupId) {
return {
errors: [{ detail: "Provider Group ID is required." }],
};
}
const url = new URL(`${apiBaseUrl}/provider-groups/${providerGroupId}`);
try {
const response = await fetch(url.toString(), {
method: "DELETE",
headers,
});
if (!response.ok) {
try {
const errorData = await response.json();
throw new Error(
errorData?.message || "Failed to delete the provider group",
);
} catch {
throw new Error("Failed to delete the provider group");
}
}
let data = null;
if (response.status !== 204) {
data = await response.json();
}
revalidatePath("/providers");
return data || { success: true };
} catch (error) {
console.error("Error deleting provider group:", error);
const message = getErrorMessage(error);
return { errors: [{ detail: message }] };
}
};