feat(ui): add resourceType field and update resource EntityInfo in drawer

This commit is contained in:
alejandrobailo
2026-04-08 12:43:05 +02:00
parent 9df1ac01eb
commit 2db2641611
4 changed files with 104 additions and 14 deletions

View File

@@ -138,6 +138,7 @@ export function adaptFindingGroupResourcesResponse(
providerAlias: item.attributes.provider?.alias || "",
providerUid: item.attributes.provider?.uid || "",
resourceName: item.attributes.resource?.name || "-",
resourceType: item.attributes.resource?.type || "-",
resourceGroup: item.attributes.resource?.resource_group || "-",
resourceUid: item.attributes.resource?.uid || "-",
service: item.attributes.resource?.service || "-",
@@ -146,7 +147,6 @@ export function adaptFindingGroupResourcesResponse(
status: item.attributes.status,
delta: item.attributes.delta || null,
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,

View File

@@ -204,7 +204,7 @@ export function getColumnFindingResources({
<div className="max-w-[240px]">
<EntityInfo
nameIcon={<Container className="size-4" />}
entityAlias={row.original.resourceGroup}
entityAlias={row.original.resourceName}
entityId={row.original.resourceUid}
/>
</div>

View File

@@ -12,7 +12,7 @@ import {
} from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { useSearchParams } from "next/navigation";
import { useState } from "react";
import { getCompliancesOverview } from "@/actions/compliances";
@@ -84,7 +84,87 @@ function normalizeComplianceFrameworkName(framework: string): string {
return framework
.trim()
.toLowerCase()
.replace(/[\s_]+/g, "-");
.replace(/[\s_]+/g, "-")
.replace(/-+/g, "-");
}
function stripComplianceVersionSuffix(framework: string): string {
return framework.replace(/-\d+(?:\.\d+)*$/g, "");
}
function canonicalComplianceKey(framework: string): string {
return stripComplianceVersionSuffix(
normalizeComplianceFrameworkName(framework),
)
.replace(/[^a-z0-9]+/g, "")
.trim();
}
function complianceTokens(framework: string): string[] {
return stripComplianceVersionSuffix(
normalizeComplianceFrameworkName(framework),
)
.split("-")
.map((token) => token.trim())
.filter(Boolean)
.filter((token) => !/^\d+(?:\.\d+)*$/.test(token));
}
function complianceMatchScore(
sourceFramework: string,
targetFramework: string,
): number {
const normalizedSource = normalizeComplianceFrameworkName(sourceFramework);
const normalizedTarget = normalizeComplianceFrameworkName(targetFramework);
if (normalizedSource === normalizedTarget) {
return 5;
}
const canonicalSource = canonicalComplianceKey(sourceFramework);
const canonicalTarget = canonicalComplianceKey(targetFramework);
if (canonicalSource === canonicalTarget) {
return 4;
}
if (
canonicalSource &&
canonicalTarget &&
(canonicalTarget.startsWith(canonicalSource) ||
canonicalSource.startsWith(canonicalTarget))
) {
return 3;
}
const sourceTokens = complianceTokens(sourceFramework);
const targetTokens = complianceTokens(targetFramework);
if (!sourceTokens.length || !targetTokens.length) {
return 0;
}
const sourceMatchesTarget = sourceTokens.every((token) =>
targetTokens.includes(token),
);
const targetMatchesSource = targetTokens.every((token) =>
sourceTokens.includes(token),
);
if (sourceMatchesTarget || targetMatchesSource) {
return 2;
}
if (
sourceTokens.some((token) => targetTokens.includes(token)) &&
canonicalSource &&
canonicalTarget &&
(canonicalTarget.includes(canonicalSource) ||
canonicalSource.includes(canonicalTarget))
) {
return 1;
}
return 0;
}
function parseSelectedScanIds(scanFilterValue: string | null): string[] {
@@ -110,12 +190,13 @@ function resolveComplianceMatch(
return null;
}
const normalizedFramework = normalizeComplianceFrameworkName(framework);
const match = compliances.find(
(compliance) =>
normalizeComplianceFrameworkName(compliance.attributes.framework) ===
normalizedFramework,
);
const match = compliances
.map((compliance) => ({
compliance,
score: complianceMatchScore(framework, compliance.attributes.framework),
}))
.filter(({ score }) => score > 0)
.sort((a, b) => b.score - a.score)[0]?.compliance;
if (!match) {
return null;
@@ -202,7 +283,6 @@ export function ResourceDetailDrawerContent({
onNavigateNext,
onMuteComplete,
}: ResourceDetailDrawerContentProps) {
const router = useRouter();
const searchParams = useSearchParams();
const [isMuteModalOpen, setIsMuteModalOpen] = useState(false);
const [isJiraModalOpen, setIsJiraModalOpen] = useState(false);
@@ -284,7 +364,7 @@ export function ResourceDetailDrawerContent({
return;
}
router.push(
window.open(
buildComplianceDetailHref({
complianceId: complianceMatch.complianceId,
framework: complianceMatch.framework,
@@ -294,6 +374,8 @@ export function ResourceDetailDrawerContent({
currentFinding: f,
includeScanData: f?.scan?.id === complianceScanId,
}),
"_blank",
"noopener,noreferrer",
);
} catch (error) {
console.error("Error resolving compliance detail:", error);
@@ -477,7 +559,7 @@ export function ResourceDetailDrawerContent({
/>
<EntityInfo
nameIcon={<Container className="size-4" />}
entityAlias={f.resourceGroup}
entityAlias={f.resourceName}
entityId={f.resourceUid}
idLabel="UID"
/>
@@ -505,7 +587,9 @@ export function ResourceDetailDrawerContent({
<InfoField label="Failing for" variant="compact">
{getFailingForLabel(f.firstSeenAt) || "-"}
</InfoField>
<div className="hidden md:block" />
<InfoField label="Group" variant="compact">
{f.resourceGroup || "-"}
</InfoField>
{/* Row 3: IDs */}
<InfoField label="Check ID" variant="compact">
@@ -529,6 +613,11 @@ export function ResourceDetailDrawerContent({
className="max-w-full text-sm"
/>
</InfoField>
{/* Row 4: Resource metadata */}
<InfoField label="Resource type" variant="compact">
{f.resourceType || "-"}
</InfoField>
</div>
{/* Actions button — fixed size, aligned with row 1 */}

View File

@@ -34,6 +34,7 @@ export interface FindingResourceRow {
providerAlias: string;
providerUid: string;
resourceName: string;
resourceType: string;
resourceGroup: string;
resourceUid: string;
service: string;