mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 04:51:51 +00:00
fix(ui): refine grouped Jira upgrade prompts
This commit is contained in:
@@ -265,7 +265,7 @@ describe("FloatingMuteButton — onBeforeOpen error handling", () => {
|
||||
).toHaveTextContent("Send 1 Group and 1 Finding to Jira");
|
||||
});
|
||||
|
||||
it("should show the Cloud Jira upgrade below the action label", async () => {
|
||||
it("should show the Cloud Jira tooltip and open the upgrade modal", async () => {
|
||||
// Given
|
||||
const onSendToJira = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
@@ -287,15 +287,20 @@ describe("FloatingMuteButton — onBeforeOpen error handling", () => {
|
||||
// Then
|
||||
expect(jiraAction).toBeVisible();
|
||||
expect(jiraAction).not.toHaveAttribute("aria-disabled");
|
||||
|
||||
const jiraLabel = within(jiraAction).getByText("Send to Jira");
|
||||
const cloudBadge = within(jiraAction).getByText(
|
||||
"Available only in Prowler Cloud",
|
||||
);
|
||||
expect(jiraLabel.nextElementSibling).toContainElement(cloudBadge);
|
||||
expect(
|
||||
within(jiraAction).queryByText("Available only in Prowler Cloud"),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
// When
|
||||
await user.click(cloudBadge);
|
||||
await user.hover(jiraAction);
|
||||
|
||||
// Then
|
||||
expect(await screen.findByRole("tooltip")).toHaveTextContent(
|
||||
"Available only in Prowler Cloud",
|
||||
);
|
||||
|
||||
// When
|
||||
await user.click(jiraAction);
|
||||
|
||||
// Then
|
||||
expect(useCloudUpgradeStore.getState().activeFeature).toBe(
|
||||
|
||||
@@ -6,7 +6,6 @@ import { createPortal } from "react-dom";
|
||||
|
||||
import { JiraIcon } from "@/components/icons/services/IconServices";
|
||||
import { Button } from "@/components/shadcn";
|
||||
import { Badge } from "@/components/shadcn/badge/badge";
|
||||
import {
|
||||
ActionDropdown,
|
||||
ActionDropdownItem,
|
||||
@@ -40,10 +39,6 @@ interface FloatingMuteButtonProps {
|
||||
sendToJiraLabel?: string;
|
||||
}
|
||||
|
||||
const CloudFeatureBadge = () => (
|
||||
<Badge variant="cloud">{PROWLER_CLOUD_ONLY_TOOLTIP}</Badge>
|
||||
);
|
||||
|
||||
export function FloatingMuteButton({
|
||||
selectedCount,
|
||||
selectedFindingIds,
|
||||
@@ -168,8 +163,8 @@ export function FloatingMuteButton({
|
||||
<ActionDropdownItem
|
||||
icon={<JiraIcon size={20} />}
|
||||
label={sendToJiraLabel}
|
||||
description={
|
||||
!canSendToJira ? <CloudFeatureBadge /> : undefined
|
||||
tooltip={
|
||||
!canSendToJira ? PROWLER_CLOUD_ONLY_TOOLTIP : undefined
|
||||
}
|
||||
aria-label={sendToJiraLabel}
|
||||
onSelect={handleJiraClick}
|
||||
|
||||
@@ -2,6 +2,9 @@ import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { useCloudUpgradeStore } from "@/store/cloud-upgrade/store";
|
||||
import { CLOUD_UPGRADE_FEATURE } from "@/types/cloud-upgrade";
|
||||
|
||||
const { MuteFindingsModalMock, isGroupedJiraDispatchEnabledMock } = vi.hoisted(
|
||||
() => ({
|
||||
MuteFindingsModalMock: vi.fn(() => null),
|
||||
@@ -43,13 +46,19 @@ vi.mock("@/components/shadcn/dropdown", () => ({
|
||||
onSelect,
|
||||
disabled,
|
||||
disabledTooltip,
|
||||
tooltip,
|
||||
}: {
|
||||
label: string;
|
||||
onSelect?: () => void;
|
||||
disabled?: boolean;
|
||||
disabledTooltip?: string;
|
||||
tooltip?: string;
|
||||
}) => (
|
||||
<button onClick={onSelect} disabled={disabled} title={disabledTooltip}>
|
||||
<button
|
||||
onClick={onSelect}
|
||||
disabled={disabled}
|
||||
title={tooltip ?? disabledTooltip}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
),
|
||||
@@ -149,6 +158,7 @@ describe("DataTableRowActions", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
isGroupedJiraDispatchEnabledMock.mockReturnValue(true);
|
||||
useCloudUpgradeStore.getState().closeCloudUpgrade();
|
||||
});
|
||||
|
||||
it("opens the mute modal immediately in preparing state for finding groups", async () => {
|
||||
@@ -319,7 +329,7 @@ describe("DataTableRowActions", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("shows disabled Cloud-only Jira action for finding groups outside cloud", async () => {
|
||||
it("shows the Cloud tooltip and opens the upgrade modal for groups outside cloud", async () => {
|
||||
// Given
|
||||
isGroupedJiraDispatchEnabledMock.mockReturnValue(false);
|
||||
const user = userEvent.setup();
|
||||
@@ -359,11 +369,14 @@ describe("DataTableRowActions", () => {
|
||||
|
||||
// Then
|
||||
expect(jiraButton).toBeVisible();
|
||||
expect(jiraButton).toBeDisabled();
|
||||
expect(jiraButton).toBeEnabled();
|
||||
expect(jiraButton).toHaveAttribute(
|
||||
"title",
|
||||
"Available only in Prowler Cloud",
|
||||
);
|
||||
expect(useCloudUpgradeStore.getState().activeFeature).toBe(
|
||||
CLOUD_UPGRADE_FEATURE.JIRA_DISPATCH,
|
||||
);
|
||||
expect(SendToJiraModalMock).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ isOpen: true }),
|
||||
undefined,
|
||||
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
import { isFindingGroupMuted } from "@/lib/findings-groups";
|
||||
import { createJiraTargetSelection } from "@/lib/jira-dispatch-selection";
|
||||
import { getOptionalText } from "@/lib/utils";
|
||||
import { useCloudUpgradeStore } from "@/store";
|
||||
import { CLOUD_UPGRADE_FEATURE } from "@/types/cloud-upgrade";
|
||||
import type {
|
||||
FindingTriageLoadedNote,
|
||||
FindingTriageSummary,
|
||||
@@ -121,6 +123,9 @@ export function DataTableRowActions<T extends FindingRowData>({
|
||||
const [mutePreparationError, setMutePreparationError] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const openCloudUpgrade = useCloudUpgradeStore(
|
||||
(state) => state.openCloudUpgrade,
|
||||
);
|
||||
|
||||
const { isMuted, canMute, title: findingTitle } = extractRowInfo(finding);
|
||||
const resolvedFindingContext = findingContext ?? {
|
||||
@@ -196,7 +201,7 @@ export function DataTableRowActions<T extends FindingRowData>({
|
||||
jiraTargetIds,
|
||||
jiraTargetType,
|
||||
);
|
||||
const isJiraActionDisabled =
|
||||
const requiresJiraUpgrade =
|
||||
(isGroup || jiraTargetIds.length > 1) && !groupedJiraDispatchEnabled;
|
||||
const selectedJiraResourceCount = isGroup
|
||||
? (finding.resourcesFail ?? 0)
|
||||
@@ -273,7 +278,10 @@ export function DataTableRowActions<T extends FindingRowData>({
|
||||
};
|
||||
|
||||
const handleJiraClick = () => {
|
||||
if (isJiraActionDisabled) return;
|
||||
if (requiresJiraUpgrade) {
|
||||
openCloudUpgrade(CLOUD_UPGRADE_FEATURE.JIRA_DISPATCH);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsJiraModalOpen(true);
|
||||
};
|
||||
@@ -332,8 +340,9 @@ export function DataTableRowActions<T extends FindingRowData>({
|
||||
<ActionDropdownItem
|
||||
icon={<JiraIcon size={20} />}
|
||||
label={getJiraLabel()}
|
||||
disabled={isJiraActionDisabled}
|
||||
disabledTooltip={PROWLER_CLOUD_ONLY_TOOLTIP}
|
||||
tooltip={
|
||||
requiresJiraUpgrade ? PROWLER_CLOUD_ONLY_TOOLTIP : undefined
|
||||
}
|
||||
onSelect={handleJiraClick}
|
||||
/>
|
||||
</ActionDropdown>
|
||||
|
||||
@@ -170,7 +170,6 @@ vi.mock("./inline-resource-container", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../floating-mute-button", () => ({
|
||||
PROWLER_CLOUD_ONLY_TOOLTIP: "Available only in Prowler Cloud",
|
||||
FloatingMuteButton: ({
|
||||
label,
|
||||
muteLabel,
|
||||
@@ -204,10 +203,12 @@ vi.mock("../floating-mute-button", () => ({
|
||||
<button
|
||||
type="button"
|
||||
aria-label={sendToJiraLabel ?? "Send to Jira"}
|
||||
title={
|
||||
canSendToJira ? undefined : "Available only in Prowler Cloud"
|
||||
}
|
||||
onClick={() => canSendToJira && onSendToJira?.()}
|
||||
>
|
||||
{sendToJiraLabel ?? "Send to Jira"}
|
||||
{!canSendToJira && <span>Available only in Prowler Cloud</span>}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -913,7 +914,7 @@ describe("FindingsGroupTable", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should show selected multi-finding Jira upgrade when grouped dispatch is disabled", async () => {
|
||||
it("should show selected multi-finding Jira tooltip when grouped dispatch is disabled", async () => {
|
||||
// Given
|
||||
isGroupedJiraDispatchEnabledMock.mockReturnValue(false);
|
||||
const user = userEvent.setup();
|
||||
@@ -949,16 +950,20 @@ describe("FindingsGroupTable", () => {
|
||||
|
||||
// Then
|
||||
expect(jiraButton).not.toBeDisabled();
|
||||
expect(jiraButton).toHaveAttribute(
|
||||
"title",
|
||||
"Available only in Prowler Cloud",
|
||||
);
|
||||
expect(
|
||||
within(jiraButton).getByText("Available only in Prowler Cloud"),
|
||||
).toBeVisible();
|
||||
within(jiraButton).queryByText("Available only in Prowler Cloud"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(SendToJiraModalMock).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ isOpen: true }),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("should render Cloud-only bulk Jira upgrade when grouped Jira dispatch is disabled", async () => {
|
||||
it("should render Cloud-only bulk Jira tooltip when grouped Jira dispatch is disabled", async () => {
|
||||
// Given
|
||||
const user = userEvent.setup();
|
||||
const data = [
|
||||
@@ -989,9 +994,13 @@ describe("FindingsGroupTable", () => {
|
||||
});
|
||||
expect(jiraButton).toBeVisible();
|
||||
expect(jiraButton).not.toBeDisabled();
|
||||
expect(jiraButton).toHaveAttribute(
|
||||
"title",
|
||||
"Available only in Prowler Cloud",
|
||||
);
|
||||
expect(
|
||||
within(jiraButton).getByText("Available only in Prowler Cloud"),
|
||||
).toBeVisible();
|
||||
within(jiraButton).queryByText("Available only in Prowler Cloud"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Mute 1 Group" }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
@@ -102,9 +102,11 @@ interface ActionDropdownItemProps
|
||||
/** Main label text */
|
||||
label: ReactNode;
|
||||
/** Optional description text below the label */
|
||||
description?: ReactNode;
|
||||
description?: string;
|
||||
/** Whether the item is destructive (danger styling) */
|
||||
destructive?: boolean;
|
||||
/** Tooltip shown while the item remains interactive. */
|
||||
tooltip?: string;
|
||||
/** Tooltip shown when the item is disabled. */
|
||||
disabledTooltip?: string;
|
||||
}
|
||||
@@ -115,6 +117,7 @@ export function ActionDropdownItem({
|
||||
description,
|
||||
destructive = false,
|
||||
className,
|
||||
tooltip,
|
||||
disabledTooltip,
|
||||
disabled,
|
||||
onSelect,
|
||||
@@ -166,11 +169,13 @@ export function ActionDropdownItem({
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
|
||||
if (disabled && disabledTooltip) {
|
||||
const tooltipContent = tooltip ?? (disabled ? disabledTooltip : undefined);
|
||||
|
||||
if (tooltipContent) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{item}</TooltipTrigger>
|
||||
<TooltipContent>{disabledTooltip}</TooltipContent>
|
||||
<TooltipContent>{tooltipContent}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user