mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-23 20:42:02 +00:00
fix(ui): improve Cloud Jira upgrade action
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { render, screen, waitFor, within } 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";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hoist mocks to avoid deep dependency chains
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -42,6 +45,7 @@ function deferredPromise<T>() {
|
||||
describe("FloatingMuteButton — onBeforeOpen error handling", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useCloudUpgradeStore.getState().closeCloudUpgrade();
|
||||
});
|
||||
|
||||
it("should reset isResolving (re-enable button) when onBeforeOpen rejects", async () => {
|
||||
@@ -261,7 +265,7 @@ describe("FloatingMuteButton — onBeforeOpen error handling", () => {
|
||||
).toHaveTextContent("Send 1 Group and 1 Finding to Jira");
|
||||
});
|
||||
|
||||
it("should show disabled Cloud-only Jira action in the chooser", async () => {
|
||||
it("should show the Cloud Jira upgrade below the action label", async () => {
|
||||
// Given
|
||||
const onSendToJira = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
@@ -282,25 +286,21 @@ describe("FloatingMuteButton — onBeforeOpen error handling", () => {
|
||||
|
||||
// Then
|
||||
expect(jiraAction).toBeVisible();
|
||||
expect(jiraAction).toHaveAttribute("aria-disabled", "true");
|
||||
const cloudBadgeLink = screen.getByRole("link", {
|
||||
name: "Available only in Prowler Cloud",
|
||||
});
|
||||
expect(cloudBadgeLink).toHaveTextContent("Available only in Prowler Cloud");
|
||||
expect(cloudBadgeLink).toHaveAttribute(
|
||||
"href",
|
||||
"https://prowler.com/pricing",
|
||||
);
|
||||
expect(jiraAction).toContainElement(cloudBadgeLink);
|
||||
expect(onSendToJira).not.toHaveBeenCalled();
|
||||
expect(jiraAction).not.toHaveAttribute("aria-disabled");
|
||||
|
||||
// When
|
||||
await user.hover(jiraAction);
|
||||
|
||||
// Then
|
||||
const tooltipTexts = await screen.findAllByText(
|
||||
const jiraLabel = within(jiraAction).getByText("Send to Jira");
|
||||
const cloudBadge = within(jiraAction).getByText(
|
||||
"Available only in Prowler Cloud",
|
||||
);
|
||||
expect(tooltipTexts[0]).toBeVisible();
|
||||
expect(jiraLabel.nextElementSibling).toContainElement(cloudBadge);
|
||||
|
||||
// When
|
||||
await user.click(cloudBadge);
|
||||
|
||||
// Then
|
||||
expect(useCloudUpgradeStore.getState().activeFeature).toBe(
|
||||
CLOUD_UPGRADE_FEATURE.JIRA_DISPATCH,
|
||||
);
|
||||
expect(onSendToJira).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
} from "@/components/shadcn/dropdown/action-dropdown";
|
||||
import { Spinner } from "@/components/shadcn/spinner/spinner";
|
||||
import { PROWLER_CLOUD_ONLY_TOOLTIP } from "@/lib/deployment";
|
||||
import { useCloudUpgradeStore } from "@/store";
|
||||
import { CLOUD_UPGRADE_FEATURE } from "@/types/cloud-upgrade";
|
||||
|
||||
import { MuteFindingsModal } from "./mute-findings-modal";
|
||||
|
||||
@@ -36,20 +38,10 @@ interface FloatingMuteButtonProps {
|
||||
showSendToJira?: boolean;
|
||||
/** Custom Jira action label. Defaults to "Send to Jira". */
|
||||
sendToJiraLabel?: string;
|
||||
/** Tooltip shown when the Jira action is visible but disabled. */
|
||||
jiraDisabledTooltip?: string;
|
||||
}
|
||||
|
||||
const CloudFeatureBadgeLink = () => (
|
||||
<Badge variant="cloud" asChild>
|
||||
<a
|
||||
href="https://prowler.com/pricing"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{PROWLER_CLOUD_ONLY_TOOLTIP}
|
||||
</a>
|
||||
</Badge>
|
||||
const CloudFeatureBadge = () => (
|
||||
<Badge variant="cloud">{PROWLER_CLOUD_ONLY_TOOLTIP}</Badge>
|
||||
);
|
||||
|
||||
export function FloatingMuteButton({
|
||||
@@ -64,7 +56,6 @@ export function FloatingMuteButton({
|
||||
canSendToJira = false,
|
||||
showSendToJira = canSendToJira,
|
||||
sendToJiraLabel = "Send to Jira",
|
||||
jiraDisabledTooltip = PROWLER_CLOUD_ONLY_TOOLTIP,
|
||||
}: FloatingMuteButtonProps) {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [resolvedIds, setResolvedIds] = useState<string[]>([]);
|
||||
@@ -73,6 +64,9 @@ export function FloatingMuteButton({
|
||||
const [mutePreparationError, setMutePreparationError] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const openCloudUpgrade = useCloudUpgradeStore(
|
||||
(state) => state.openCloudUpgrade,
|
||||
);
|
||||
|
||||
const handleModalOpenChange = (
|
||||
nextOpen: boolean | ((previousOpen: boolean) => boolean),
|
||||
@@ -117,7 +111,10 @@ export function FloatingMuteButton({
|
||||
};
|
||||
|
||||
const handleJiraClick = () => {
|
||||
if (!canSendToJira) return;
|
||||
if (!canSendToJira) {
|
||||
openCloudUpgrade(CLOUD_UPGRADE_FEATURE.JIRA_DISPATCH);
|
||||
return;
|
||||
}
|
||||
|
||||
onSendToJira?.();
|
||||
};
|
||||
@@ -170,15 +167,11 @@ export function FloatingMuteButton({
|
||||
/>
|
||||
<ActionDropdownItem
|
||||
icon={<JiraIcon size={20} />}
|
||||
label={
|
||||
<span className="flex items-center gap-2">
|
||||
{sendToJiraLabel}
|
||||
{!canSendToJira && <CloudFeatureBadgeLink />}
|
||||
</span>
|
||||
label={sendToJiraLabel}
|
||||
description={
|
||||
!canSendToJira ? <CloudFeatureBadge /> : undefined
|
||||
}
|
||||
aria-label={sendToJiraLabel}
|
||||
disabled={!canSendToJira}
|
||||
disabledTooltip={jiraDisabledTooltip}
|
||||
onSelect={handleJiraClick}
|
||||
/>
|
||||
</ActionDropdown>
|
||||
|
||||
@@ -26,10 +26,7 @@ import {
|
||||
import { SeverityBadge, StatusFindingBadge } from "@/components/shadcn/table";
|
||||
import { useFindingGroupResourceState } from "@/hooks/use-finding-group-resource-state";
|
||||
import { cn, hasHistoricalFindingFilter } from "@/lib";
|
||||
import {
|
||||
isGroupedJiraDispatchEnabled,
|
||||
PROWLER_CLOUD_ONLY_TOOLTIP,
|
||||
} from "@/lib/deployment";
|
||||
import { isGroupedJiraDispatchEnabled } from "@/lib/deployment";
|
||||
import {
|
||||
getFilteredFindingGroupDelta,
|
||||
getFindingGroupImpactedCounts,
|
||||
@@ -259,7 +256,6 @@ export function FindingsGroupDrillDown({
|
||||
isBulkOperation={selectedFindingIds.length > 1}
|
||||
showSendToJira
|
||||
canSendToJira={canSendSelectedFindingsToJira}
|
||||
jiraDisabledTooltip={PROWLER_CLOUD_ONLY_TOOLTIP}
|
||||
sendToJiraLabel={`Send ${selectedFindingIds.length} ${
|
||||
selectedFindingIds.length === 1 ? "Finding" : "Findings"
|
||||
} to Jira`}
|
||||
|
||||
@@ -179,7 +179,6 @@ vi.mock("../floating-mute-button", () => ({
|
||||
onSendToJira,
|
||||
canSendToJira,
|
||||
showSendToJira,
|
||||
jiraDisabledTooltip,
|
||||
}: {
|
||||
label?: string;
|
||||
muteLabel?: string;
|
||||
@@ -188,7 +187,6 @@ vi.mock("../floating-mute-button", () => ({
|
||||
onSendToJira?: () => void;
|
||||
canSendToJira?: boolean;
|
||||
showSendToJira?: boolean;
|
||||
jiraDisabledTooltip?: string;
|
||||
}) => {
|
||||
const [isChooserOpen, setIsChooserOpen] = useState(false);
|
||||
|
||||
@@ -205,11 +203,11 @@ vi.mock("../floating-mute-button", () => ({
|
||||
{showSendToJira && (
|
||||
<button
|
||||
type="button"
|
||||
disabled={!canSendToJira}
|
||||
title={!canSendToJira ? jiraDisabledTooltip : undefined}
|
||||
onClick={onSendToJira}
|
||||
aria-label={sendToJiraLabel ?? "Send to Jira"}
|
||||
onClick={() => canSendToJira && onSendToJira?.()}
|
||||
>
|
||||
{sendToJiraLabel ?? "Send to Jira"}
|
||||
{!canSendToJira && <span>Available only in Prowler Cloud</span>}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -915,7 +913,7 @@ describe("FindingsGroupTable", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should disable selected multi-finding Jira dispatch when grouped dispatch is disabled", async () => {
|
||||
it("should show selected multi-finding Jira upgrade when grouped dispatch is disabled", async () => {
|
||||
// Given
|
||||
isGroupedJiraDispatchEnabledMock.mockReturnValue(false);
|
||||
const user = userEvent.setup();
|
||||
@@ -950,18 +948,17 @@ describe("FindingsGroupTable", () => {
|
||||
await user.click(jiraButton);
|
||||
|
||||
// Then
|
||||
expect(jiraButton).toBeDisabled();
|
||||
expect(jiraButton).toHaveAttribute(
|
||||
"title",
|
||||
"Available only in Prowler Cloud",
|
||||
);
|
||||
expect(jiraButton).not.toBeDisabled();
|
||||
expect(
|
||||
within(jiraButton).getByText("Available only in Prowler Cloud"),
|
||||
).toBeVisible();
|
||||
expect(SendToJiraModalMock).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ isOpen: true }),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("should render disabled Cloud-only bulk Jira button when grouped Jira dispatch is disabled", async () => {
|
||||
it("should render Cloud-only bulk Jira upgrade when grouped Jira dispatch is disabled", async () => {
|
||||
// Given
|
||||
const user = userEvent.setup();
|
||||
const data = [
|
||||
@@ -991,11 +988,10 @@ describe("FindingsGroupTable", () => {
|
||||
name: "Send 1 Group to Jira",
|
||||
});
|
||||
expect(jiraButton).toBeVisible();
|
||||
expect(jiraButton).toBeDisabled();
|
||||
expect(jiraButton).toHaveAttribute(
|
||||
"title",
|
||||
"Available only in Prowler Cloud",
|
||||
);
|
||||
expect(jiraButton).not.toBeDisabled();
|
||||
expect(
|
||||
within(jiraButton).getByText("Available only in Prowler Cloud"),
|
||||
).toBeVisible();
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Mute 1 Group" }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
@@ -9,10 +9,7 @@ import { CustomCheckboxMutedFindings } from "@/components/filters/custom-checkbo
|
||||
import { SendToJiraModal } from "@/components/findings/send-to-jira-modal";
|
||||
import { OnboardingTrigger, PageReady } from "@/components/onboarding";
|
||||
import { DataTable } from "@/components/shadcn/table";
|
||||
import {
|
||||
isGroupedJiraDispatchEnabled,
|
||||
PROWLER_CLOUD_ONLY_TOOLTIP,
|
||||
} from "@/lib/deployment";
|
||||
import { isGroupedJiraDispatchEnabled } from "@/lib/deployment";
|
||||
import { canDrillDownFindingGroup } from "@/lib/findings-groups";
|
||||
import {
|
||||
createJiraBatchSelection,
|
||||
@@ -443,7 +440,6 @@ const FindingsGroupTableContent = ({
|
||||
showSendToJira={hasJiraTargets}
|
||||
canSendToJira={canSendToJira}
|
||||
sendToJiraLabel={sendToJiraLabel}
|
||||
jiraDisabledTooltip={PROWLER_CLOUD_ONLY_TOOLTIP}
|
||||
onSendToJira={() => setIsJiraModalOpen(true)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -102,7 +102,7 @@ interface ActionDropdownItemProps
|
||||
/** Main label text */
|
||||
label: ReactNode;
|
||||
/** Optional description text below the label */
|
||||
description?: string;
|
||||
description?: ReactNode;
|
||||
/** Whether the item is destructive (danger styling) */
|
||||
destructive?: boolean;
|
||||
/** Tooltip shown when the item is disabled. */
|
||||
|
||||
@@ -85,6 +85,38 @@ describe("CloudUpgradeModal", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("renders the contextual Jira dispatch upgrade", async () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
useCloudUpgradeStore
|
||||
.getState()
|
||||
.openCloudUpgrade(CLOUD_UPGRADE_FEATURE.JIRA_DISPATCH);
|
||||
|
||||
// When
|
||||
render(<CloudUpgradeModal />);
|
||||
|
||||
// Then
|
||||
expect(
|
||||
await screen.findByRole("dialog", {
|
||||
name: "Send Findings to Jira at Scale",
|
||||
}),
|
||||
).toBeVisible();
|
||||
expect(
|
||||
screen.getByRole("link", {
|
||||
name: "Send Findings to Jira in Prowler Cloud",
|
||||
}),
|
||||
).toHaveAttribute(
|
||||
"href",
|
||||
"https://cloud.prowler.com/sign-up?utm_source=prowler-local-server&utm_content=jira-dispatch",
|
||||
);
|
||||
expect(
|
||||
screen.getByRole("link", { name: "View Plans & Pricing" }),
|
||||
).toHaveAttribute(
|
||||
"href",
|
||||
"https://prowler.com/pricing?utm_source=prowler-local-server&utm_content=jira-dispatch",
|
||||
);
|
||||
});
|
||||
|
||||
it("closes the active upgrade and returns focus to its trigger", async () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
|
||||
@@ -24,6 +24,7 @@ describe("cloud upgrade content", () => {
|
||||
"Bring CLI Findings into One Cloud View",
|
||||
"See Compliance Across Every Provider",
|
||||
"Coordinate Finding Remediation",
|
||||
"Send Findings to Jira at Scale",
|
||||
"Use The Agent Cloud Defender",
|
||||
"Scale Prowler Without Operating It",
|
||||
"Configure Every Scan Once",
|
||||
@@ -71,6 +72,7 @@ describe("cloud upgrade URLs", () => {
|
||||
"cross-provider-compliance",
|
||||
],
|
||||
[CLOUD_UPGRADE_FEATURE.FINDING_TRIAGE, "findings"],
|
||||
[CLOUD_UPGRADE_FEATURE.JIRA_DISPATCH, "jira-dispatch"],
|
||||
[CLOUD_UPGRADE_FEATURE.LIGHTHOUSE_AI, "lighthouse-ai"],
|
||||
[CLOUD_UPGRADE_FEATURE.GENERAL, "general"],
|
||||
[CLOUD_UPGRADE_FEATURE.SCAN_CONFIGURATION, "scan-configuration"],
|
||||
|
||||
@@ -26,6 +26,7 @@ const CLOUD_UPGRADE_UTM_CONTENT = {
|
||||
[CLOUD_UPGRADE_FEATURE.CROSS_PROVIDER_COMPLIANCE]:
|
||||
"cross-provider-compliance",
|
||||
[CLOUD_UPGRADE_FEATURE.FINDING_TRIAGE]: "findings",
|
||||
[CLOUD_UPGRADE_FEATURE.JIRA_DISPATCH]: "jira-dispatch",
|
||||
[CLOUD_UPGRADE_FEATURE.LIGHTHOUSE_AI]: "lighthouse-ai",
|
||||
[CLOUD_UPGRADE_FEATURE.GENERAL]: "general",
|
||||
[CLOUD_UPGRADE_FEATURE.SCAN_CONFIGURATION]: "scan-configuration",
|
||||
@@ -98,6 +99,17 @@ export const CLOUD_UPGRADE_CONTENT = {
|
||||
],
|
||||
primaryCta: "Triage Findings in Prowler Cloud",
|
||||
},
|
||||
[CLOUD_UPGRADE_FEATURE.JIRA_DISPATCH]: {
|
||||
title: "Send Findings to Jira at Scale",
|
||||
description:
|
||||
"Create Jira issues from selected findings and finding groups without handling each item separately.",
|
||||
benefits: [
|
||||
"Send selected findings or finding groups in one action",
|
||||
"Choose between grouped and individual Jira issues",
|
||||
"Track dispatch progress and retry failed findings",
|
||||
],
|
||||
primaryCta: "Send Findings to Jira in Prowler Cloud",
|
||||
},
|
||||
[CLOUD_UPGRADE_FEATURE.LIGHTHOUSE_AI]: {
|
||||
title: "Use The Agent Cloud Defender",
|
||||
description:
|
||||
|
||||
@@ -5,6 +5,7 @@ export const CLOUD_UPGRADE_FEATURE = {
|
||||
CLI_IMPORT: "cli_import",
|
||||
CROSS_PROVIDER_COMPLIANCE: "cross_provider_compliance",
|
||||
FINDING_TRIAGE: "finding_triage",
|
||||
JIRA_DISPATCH: "jira_dispatch",
|
||||
LIGHTHOUSE_AI: "lighthouse_ai",
|
||||
GENERAL: "general",
|
||||
SCAN_CONFIGURATION: "scan_configuration",
|
||||
|
||||
Reference in New Issue
Block a user