Files
prowler/ui/actions/finding-groups/finding-groups.adapter.ts
Alan Buscaglia 1bf13bf567 fix(ui): code quality improvements for findings grouped view
- Replace `any` with `unknown` + type guards in finding-groups and
  findings-by-resource adapters (4 adapter functions hardened)
- Add unmount cleanup for AbortController in use-resource-detail-drawer
  to prevent memory leaks on mid-flight unmount
- Add try/catch/finally error handling for onBeforeOpen in floating mute
  button to prevent permanent spinner state on network errors
- Replace hardcoded colors (border-gray-300, bg-white, text-slate-950)
  with semantic tokens (border-default-200, bg-background, text-foreground)
  for dark mode compatibility
- Change non-accessible `<p onClick>` to `<button>` with w-full and
  type=button in column-finding-groups for keyboard accessibility
- Fix region flag mapping: asia-east1 (Taiwan) now correctly shows flag_tw
  instead of flag_hk by adding specific rule before Hong Kong regex
- Fix import type usage in adapter files (value imports preserved for
  runtime constants like FINDINGS_ROW_TYPE)
- Restore pageRef.current = 1 in refresh() for error path safety

43 new tests across 7 test files. 50 total tests passing.
TypeScript: 0 errors. ESLint: 0 errors.
Judgment Day: 2 rounds, 0 discrepancies remaining.
2026-03-30 15:45:01 +02:00

153 lines
4.4 KiB
TypeScript

import type {
FindingGroupRow,
FindingResourceRow,
FindingStatus,
ProviderType,
Severity,
} from "@/types";
import { FINDINGS_ROW_TYPE } from "@/types";
/**
* API response shape for a finding group (JSON:API).
* Each group represents a unique check_id with aggregated counts.
*
* Fields come from FindingGroupSerializer which aggregates
* FindingGroupDailySummary rows by check_id.
*/
interface FindingGroupAttributes {
check_id: string;
check_title: string | null;
check_description: string | null;
severity: string;
status: string; // "FAIL" | "PASS" | "MUTED" (already uppercase)
impacted_providers: string[];
resources_total: number;
resources_fail: number;
pass_count: number;
fail_count: number;
muted_count: number;
new_count: number;
changed_count: number;
first_seen_at: string | null;
last_seen_at: string | null;
failing_since: string | null;
}
interface FindingGroupApiItem {
type: "finding-groups";
id: string;
attributes: FindingGroupAttributes;
}
/**
* Transforms the API response for finding groups into FindingGroupRow[].
*/
export function adaptFindingGroupsResponse(
apiResponse: unknown,
): FindingGroupRow[] {
if (
!apiResponse ||
typeof apiResponse !== "object" ||
!("data" in apiResponse) ||
!Array.isArray((apiResponse as { data: unknown }).data)
) {
return [];
}
const data = (apiResponse as { data: FindingGroupApiItem[] }).data;
return data.map((item) => ({
id: item.id,
rowType: FINDINGS_ROW_TYPE.GROUP,
checkId: item.attributes.check_id,
checkTitle: item.attributes.check_title || item.attributes.check_id,
severity: item.attributes.severity as Severity,
status: item.attributes.status as FindingStatus,
resourcesTotal: item.attributes.resources_total,
resourcesFail: item.attributes.resources_fail,
newCount: item.attributes.new_count,
changedCount: item.attributes.changed_count,
mutedCount: item.attributes.muted_count,
providers: (item.attributes.impacted_providers || []) as ProviderType[],
updatedAt: item.attributes.last_seen_at || "",
}));
}
/**
* API response shape for a finding group resource (drill-down).
* Endpoint: /finding-groups/{check_id}/resources
*
* Each item has nested `resource` and `provider` objects in attributes
* (NOT JSON:API included — it's a custom serializer).
*/
interface ResourceInfo {
uid: string;
name: string;
service: string;
region: string;
type: string;
resource_group: string;
}
interface ProviderInfo {
type: string;
uid: string;
alias: string;
}
interface FindingGroupResourceAttributes {
resource: ResourceInfo;
provider: ProviderInfo;
status: string;
severity: string;
first_seen_at: string | null;
last_seen_at: string | null;
muted_reason?: string | null;
}
interface FindingGroupResourceApiItem {
type: "finding-group-resources";
id: string;
attributes: FindingGroupResourceAttributes;
}
/**
* Transforms the API response for finding group resources (drill-down)
* into FindingResourceRow[].
*/
export function adaptFindingGroupResourcesResponse(
apiResponse: unknown,
checkId: string,
): FindingResourceRow[] {
if (
!apiResponse ||
typeof apiResponse !== "object" ||
!("data" in apiResponse) ||
!Array.isArray((apiResponse as { data: unknown }).data)
) {
return [];
}
const data = (apiResponse as { data: FindingGroupResourceApiItem[] }).data;
return data.map((item) => ({
id: item.id,
rowType: FINDINGS_ROW_TYPE.RESOURCE,
findingId: item.id,
checkId,
providerType: (item.attributes.provider?.type || "aws") as ProviderType,
providerAlias: item.attributes.provider?.alias || "",
providerUid: item.attributes.provider?.uid || "",
resourceName: item.attributes.resource?.name || "-",
resourceGroup: item.attributes.resource?.resource_group || "-",
resourceUid: item.attributes.resource?.uid || "-",
service: item.attributes.resource?.service || "-",
region: item.attributes.resource?.region || "-",
severity: (item.attributes.severity || "informational") as Severity,
status: item.attributes.status,
isMuted: item.attributes.status === "MUTED",
// TODO: remove fallback once the API returns muted_reason in finding-group-resources
mutedReason: item.attributes.muted_reason || undefined,
firstSeenAt: item.attributes.first_seen_at,
lastSeenAt: item.attributes.last_seen_at,
}));
}