diff --git a/ui/components/findings/floating-mute-button.test.tsx b/ui/components/findings/floating-mute-button.test.tsx index 01eb306ce9..10063d14ee 100644 --- a/ui/components/findings/floating-mute-button.test.tsx +++ b/ui/components/findings/floating-mute-button.test.tsx @@ -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( diff --git a/ui/components/findings/floating-mute-button.tsx b/ui/components/findings/floating-mute-button.tsx index 696ac6bb78..c9eee25626 100644 --- a/ui/components/findings/floating-mute-button.tsx +++ b/ui/components/findings/floating-mute-button.tsx @@ -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 = () => ( - {PROWLER_CLOUD_ONLY_TOOLTIP} -); - export function FloatingMuteButton({ selectedCount, selectedFindingIds, @@ -168,8 +163,8 @@ export function FloatingMuteButton({ } label={sendToJiraLabel} - description={ - !canSendToJira ? : undefined + tooltip={ + !canSendToJira ? PROWLER_CLOUD_ONLY_TOOLTIP : undefined } aria-label={sendToJiraLabel} onSelect={handleJiraClick} diff --git a/ui/components/findings/table/data-table-row-actions.test.tsx b/ui/components/findings/table/data-table-row-actions.test.tsx index 7303d6250a..304460b3c7 100644 --- a/ui/components/findings/table/data-table-row-actions.test.tsx +++ b/ui/components/findings/table/data-table-row-actions.test.tsx @@ -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; }) => ( - ), @@ -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, diff --git a/ui/components/findings/table/data-table-row-actions.tsx b/ui/components/findings/table/data-table-row-actions.tsx index 32cfb5650c..04f82c0d73 100644 --- a/ui/components/findings/table/data-table-row-actions.tsx +++ b/ui/components/findings/table/data-table-row-actions.tsx @@ -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({ 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({ jiraTargetIds, jiraTargetType, ); - const isJiraActionDisabled = + const requiresJiraUpgrade = (isGroup || jiraTargetIds.length > 1) && !groupedJiraDispatchEnabled; const selectedJiraResourceCount = isGroup ? (finding.resourcesFail ?? 0) @@ -273,7 +278,10 @@ export function DataTableRowActions({ }; const handleJiraClick = () => { - if (isJiraActionDisabled) return; + if (requiresJiraUpgrade) { + openCloudUpgrade(CLOUD_UPGRADE_FEATURE.JIRA_DISPATCH); + return; + } setIsJiraModalOpen(true); }; @@ -332,8 +340,9 @@ export function DataTableRowActions({ } label={getJiraLabel()} - disabled={isJiraActionDisabled} - disabledTooltip={PROWLER_CLOUD_ONLY_TOOLTIP} + tooltip={ + requiresJiraUpgrade ? PROWLER_CLOUD_ONLY_TOOLTIP : undefined + } onSelect={handleJiraClick} /> diff --git a/ui/components/findings/table/findings-group-table.test.tsx b/ui/components/findings/table/findings-group-table.test.tsx index fe88817def..305bed902b 100644 --- a/ui/components/findings/table/findings-group-table.test.tsx +++ b/ui/components/findings/table/findings-group-table.test.tsx @@ -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", () => ({ )} @@ -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(); diff --git a/ui/components/shadcn/dropdown/action-dropdown.tsx b/ui/components/shadcn/dropdown/action-dropdown.tsx index a8171b6720..10c03371a8 100644 --- a/ui/components/shadcn/dropdown/action-dropdown.tsx +++ b/ui/components/shadcn/dropdown/action-dropdown.tsx @@ -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({ ); - if (disabled && disabledTooltip) { + const tooltipContent = tooltip ?? (disabled ? disabledTooltip : undefined); + + if (tooltipContent) { return ( {item} - {disabledTooltip} + {tooltipContent} ); }