From 298ad3382f0f6f885fa896d17c9f619ad768d7e4 Mon Sep 17 00:00:00 2001 From: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com> Date: Mon, 18 May 2026 15:48:24 +0100 Subject: [PATCH] chore(ui): make View Resource an icon-only link next to the resource name (#11193) Co-authored-by: alejandrobailo --- ui/CHANGELOG.md | 1 + .../resource-detail-drawer-content.test.tsx | 27 +++++++++-- .../resource-detail-drawer-content.tsx | 48 +++++++++++-------- ui/components/ui/entities/entity-info.tsx | 4 ++ 4 files changed, 57 insertions(+), 23 deletions(-) diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 1e7cded33f..fc2359a655 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -16,6 +16,7 @@ All notable changes to the **Prowler UI** are documented in this file. - Lighthouse now accepts Prowler App Finding Groups MCP tools [(#11140)](https://github.com/prowler-cloud/prowler/pull/11140) - Attack Paths graph now uses React Flow with improved layout, interactions, export, minimap, and browser test coverage [(#10686)](https://github.com/prowler-cloud/prowler/pull/10686) - SAML ACS URL is only shown if the email domain is configured [(#11144)](https://github.com/prowler-cloud/prowler/pull/11144) +- "View Resource" action in the finding resource detail drawer is now an icon-only link rendered next to the resource name (instead of a text button in the UID row), keeping the "View in AWS Console" link unchanged [(#11193)](https://github.com/prowler-cloud/prowler/pull/11193) ### 🐞 Fixed 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 ed09bb52e0..2b617888c0 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 @@ -46,10 +46,12 @@ vi.mock("next/link", () => ({ default: ({ children, href, + prefetch: _prefetch, ...props }: AnchorHTMLAttributes & { children: ReactNode; href: string; + prefetch?: boolean; }) => ( {children} @@ -288,8 +290,21 @@ vi.mock("@/components/ui/entities/date-with-time", () => ({ })); vi.mock("@/components/ui/entities/entity-info", () => ({ - EntityInfo: ({ idAction }: { idAction?: ReactNode }) => - idAction ? {idAction} : null, + EntityInfo: ({ + nameAction, + idAction, + }: { + nameAction?: ReactNode; + idAction?: ReactNode; + }) => + nameAction || idAction ? ( + + {nameAction && ( + {nameAction} + )} + {idAction && {idAction}} + + ) : null, })); vi.mock("@/components/ui/table", () => ({ @@ -428,7 +443,7 @@ const mockFinding: ResourceDrawerFinding = { }; describe("ResourceDetailDrawerContent — resource navigation", () => { - it("should render a View Resource link inline next to the resource UID", () => { + it("should render an icon-only View Resource link next to the resource name", () => { // Given render( { ); expect(viewResourceLink).toHaveAttribute("target", "_blank"); expect(viewResourceLink).toHaveAttribute("rel", "noopener noreferrer"); + // Icon-only: accessible name comes from an sr-only span, not from an + // aria-label attribute, so the text lives in the DOM (more semantic). + expect(viewResourceLink).toHaveAccessibleName("View Resource"); + expect(viewResourceLink).not.toHaveAttribute("aria-label"); + const srOnlyLabel = viewResourceLink.querySelector(".sr-only"); + expect(srOnlyLabel).toHaveTextContent("View Resource"); }); }); const mockResourceRow: FindingResourceRow = { 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 5cc3b7b298..5175e64af0 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 @@ -441,8 +441,7 @@ export function ResourceDetailDrawerContent({ findingUid: f?.uid, region: resourceRegion, }); - const hasIdAction = - Boolean(resourceDetailHref) || Boolean(externalResourceTarget); + const hasIdAction = Boolean(externalResourceTarget); const findingRecommendationUrl = f?.remediation.recommendation.url; const checkRecommendationUrl = checkMeta.remediation.recommendation.url; const recommendationUrl = isNonEmptyString(findingRecommendationUrl) @@ -712,32 +711,41 @@ export function ResourceDetailDrawerContent({ entityAlias={resourceName} entityId={resourceUid} idLabel="UID" - idAction={ - hasIdAction ? ( - - {resourceDetailHref && ( + nameAction={ + resourceDetailHref ? ( + + - )} - {externalResourceTarget && ( - - )} - + + + View Resource + + + ) : undefined + } + idAction={ + hasIdAction ? ( + ) : undefined } /> diff --git a/ui/components/ui/entities/entity-info.tsx b/ui/components/ui/entities/entity-info.tsx index 0cf4fe68f0..ff8dd39e98 100644 --- a/ui/components/ui/entities/entity-info.tsx +++ b/ui/components/ui/entities/entity-info.tsx @@ -17,6 +17,8 @@ interface EntityInfoProps { icon?: ReactNode; /** Small icon rendered inline before the entity alias text */ nameIcon?: ReactNode; + /** Inline element rendered after the entity alias (e.g. action link). */ + nameAction?: ReactNode; entityAlias?: string; entityId?: string; badge?: string; @@ -37,6 +39,7 @@ export const EntityInfo = ({ cloudProvider, icon, nameIcon, + nameAction, entityAlias, entityId, badge, @@ -74,6 +77,7 @@ export const EntityInfo = ({ ({badge}) )} + {nameAction && {nameAction}} {entityId && (