diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 25a564cbcb..2aa382a6d1 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to the **Prowler UI** are documented in this file. ### 🚀 Added - Download PDF button for CIS Benchmark compliance cards, surfaced only on the latest CIS variant per provider to match the backend's latest-only PDF generation [(#10650)](https://github.com/prowler-cloud/prowler/pull/10650) +- View Resource button in the findings resource detail drawer to open the related resource page [(#10847)](https://github.com/prowler-cloud/prowler/pull/10847) ### 🔄 Changed diff --git a/ui/components/findings/table/resource-detail-drawer/resource-detail-drawer-content.test.tsx b/ui/components/findings/table/resource-detail-drawer/resource-detail-drawer-content.test.tsx index bebc9b0c4a..2004402607 100644 --- a/ui/components/findings/table/resource-detail-drawer/resource-detail-drawer-content.test.tsx +++ b/ui/components/findings/table/resource-detail-drawer/resource-detail-drawer-content.test.tsx @@ -1,6 +1,13 @@ import { render, screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import type { ButtonHTMLAttributes, HTMLAttributes, ReactNode } from "react"; +import { + type AnchorHTMLAttributes, + type ButtonHTMLAttributes, + cloneElement, + type HTMLAttributes, + isValidElement, + type ReactNode, +} from "react"; import { createPortal } from "react-dom"; import { afterEach, describe, expect, it, vi } from "vitest"; @@ -36,8 +43,17 @@ vi.mock("next/image", () => ({ })); vi.mock("next/link", () => ({ - default: ({ children, href }: { children: ReactNode; href: string }) => ( - {children} + default: ({ + children, + href, + ...props + }: AnchorHTMLAttributes & { + children: ReactNode; + href: string; + }) => ( + + {children} + ), })); @@ -64,7 +80,12 @@ vi.mock("@/components/shadcn", () => { variant?: string; size?: string; asChild?: boolean; - }) => , + }) => + _asChild && isValidElement(children) ? ( + cloneElement(children, props) + ) : ( + + ), InfoField: ({ children, label, @@ -386,6 +407,45 @@ const mockFinding: ResourceDrawerFinding = { scan: null, }; +describe("ResourceDetailDrawerContent — resource navigation", () => { + it("should render a View Resource link below the resource actions menu", () => { + // Given + render( + , + ); + + // When + const viewResourceLink = screen.getByRole("link", { + name: "View Resource", + }); + const resourceActionsMenu = screen.getByRole("menu", { + name: "Resource actions", + }); + + // Then + expect(viewResourceLink).toHaveAttribute( + "href", + "/resources?resourceId=res-1", + ); + expect(viewResourceLink).toHaveAttribute("target", "_blank"); + expect(viewResourceLink).toHaveAttribute("rel", "noopener noreferrer"); + expect( + resourceActionsMenu.compareDocumentPosition(viewResourceLink) & + Node.DOCUMENT_POSITION_FOLLOWING, + ).not.toBe(0); + }); +}); const mockResourceRow: FindingResourceRow = { id: "row-1", rowType: "resource", diff --git a/ui/components/findings/table/resource-detail-drawer/resource-detail-drawer-content.tsx b/ui/components/findings/table/resource-detail-drawer/resource-detail-drawer-content.tsx index b1e986e635..367a3c98af 100644 --- a/ui/components/findings/table/resource-detail-drawer/resource-detail-drawer-content.tsx +++ b/ui/components/findings/table/resource-detail-drawer/resource-detail-drawer-content.tsx @@ -299,6 +299,12 @@ function buildComplianceDetailHref({ return `/compliance/${encodeURIComponent(framework)}?${params.toString()}`; } +function buildResourceDetailHref(resourceId: string): string { + const params = new URLSearchParams(); + params.set("resourceId", resourceId); + return `/resources?${params.toString()}`; +} + interface ResourceDetailDrawerContentProps { isLoading: boolean; isNavigating: boolean; @@ -416,6 +422,9 @@ export function ResourceDetailDrawerContent({ const nativeIacConfig = resolveNativeIacConfig(providerType); const showOverviewCheckMetaContent = showCheckMetaContent; const showOverviewFindingContent = Boolean(f); + const resourceDetailHref = f?.resourceId + ? buildResourceDetailHref(f.resourceId) + : null; const overviewStatusExtended = f?.statusExtended; const showOverviewStatusExtended = Boolean(overviewStatusExtended); @@ -757,6 +766,21 @@ export function ResourceDetailDrawerContent({ )} + + {resourceDetailHref && ( +
+ +
+ )} )}