chore(ui): make View Resource an icon-only link next to the resource name (#11193)

Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
This commit is contained in:
Hugo Pereira Brito
2026-05-18 15:48:24 +01:00
committed by GitHub
parent bfcbe0a9c4
commit 298ad3382f
4 changed files with 57 additions and 23 deletions
+1
View File
@@ -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
@@ -46,10 +46,12 @@ vi.mock("next/link", () => ({
default: ({
children,
href,
prefetch: _prefetch,
...props
}: AnchorHTMLAttributes<HTMLAnchorElement> & {
children: ReactNode;
href: string;
prefetch?: boolean;
}) => (
<a href={href} {...props}>
{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 ? <span data-testid="entity-id-action">{idAction}</span> : null,
EntityInfo: ({
nameAction,
idAction,
}: {
nameAction?: ReactNode;
idAction?: ReactNode;
}) =>
nameAction || idAction ? (
<span>
{nameAction && (
<span data-testid="entity-name-action">{nameAction}</span>
)}
{idAction && <span data-testid="entity-id-action">{idAction}</span>}
</span>
) : 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(
<ResourceDetailDrawerContent
@@ -457,6 +472,12 @@ describe("ResourceDetailDrawerContent — resource navigation", () => {
);
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 = {
@@ -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 ? (
<span className="inline-flex items-center gap-2">
{resourceDetailHref && (
nameAction={
resourceDetailHref ? (
<Tooltip>
<TooltipTrigger asChild>
<Button variant="link" size="link-sm" asChild>
<Link
href={resourceDetailHref}
target="_blank"
rel="noopener noreferrer"
prefetch={false}
>
View Resource
<ExternalLink className="size-3" />
<span className="sr-only">View Resource</span>
<ExternalLink
className="size-3"
aria-hidden="true"
/>
</Link>
</Button>
)}
{externalResourceTarget && (
<ExternalResourceLink
providerType={providerType}
resourceUid={resourceUid}
providerUid={providerUid}
resourceName={resourceName}
findingUid={f?.uid}
region={resourceRegion}
/>
)}
</span>
</TooltipTrigger>
<TooltipContent side="top">
View Resource
</TooltipContent>
</Tooltip>
) : undefined
}
idAction={
hasIdAction ? (
<ExternalResourceLink
providerType={providerType}
resourceUid={resourceUid}
providerUid={providerUid}
resourceName={resourceName}
findingUid={f?.uid}
region={resourceRegion}
/>
) : undefined
}
/>
@@ -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})
</span>
)}
{nameAction && <span className="shrink-0">{nameAction}</span>}
</div>
{entityId && (
<div className="flex min-w-0 items-center gap-2">