mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-23 20:42:02 +00:00
feat(ui): add View Resource action to findings drawer (#10847)
This commit is contained in:
committed by
GitHub
parent
013809919c
commit
059b71d34b
@@ -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
|
||||
|
||||
|
||||
+64
-4
@@ -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 }) => (
|
||||
<a href={href}>{children}</a>
|
||||
default: ({
|
||||
children,
|
||||
href,
|
||||
...props
|
||||
}: AnchorHTMLAttributes<HTMLAnchorElement> & {
|
||||
children: ReactNode;
|
||||
href: string;
|
||||
}) => (
|
||||
<a href={href} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}));
|
||||
|
||||
@@ -64,7 +80,12 @@ vi.mock("@/components/shadcn", () => {
|
||||
variant?: string;
|
||||
size?: string;
|
||||
asChild?: boolean;
|
||||
}) => <button {...props}>{children}</button>,
|
||||
}) =>
|
||||
_asChild && isValidElement(children) ? (
|
||||
cloneElement(children, props)
|
||||
) : (
|
||||
<button {...props}>{children}</button>
|
||||
),
|
||||
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(
|
||||
<ResourceDetailDrawerContent
|
||||
isLoading={false}
|
||||
isNavigating={false}
|
||||
checkMeta={mockCheckMeta}
|
||||
currentIndex={0}
|
||||
totalResources={1}
|
||||
currentFinding={mockFinding}
|
||||
otherFindings={[]}
|
||||
onNavigatePrev={vi.fn()}
|
||||
onNavigateNext={vi.fn()}
|
||||
onMuteComplete={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
// 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",
|
||||
|
||||
+24
@@ -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({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{resourceDetailHref && (
|
||||
<div className="border-border-neutral-secondary flex justify-end border-t pt-3">
|
||||
<Button variant="link" size="link-sm" asChild>
|
||||
<Link
|
||||
href={resourceDetailHref}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
View Resource
|
||||
<ExternalLink className="size-3" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user