mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 13:01:56 +00:00
feat(ui): add Local Server Cloud upgrade prompts (#11982)
This commit is contained in:
@@ -1,90 +0,0 @@
|
||||
import type { CSSProperties } from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface MenuFeatureBadgeProps {
|
||||
label?: string;
|
||||
variant?: "cloud" | "new";
|
||||
size?: "default" | "sm";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const FEATURE_BADGE_STYLE: Record<
|
||||
NonNullable<MenuFeatureBadgeProps["variant"]>,
|
||||
CSSProperties | undefined
|
||||
> = {
|
||||
cloud: {
|
||||
backgroundImage:
|
||||
"linear-gradient(112deg, rgb(46, 229, 155) 3.5%, rgb(98, 223, 240) 98.8%)",
|
||||
},
|
||||
new: undefined,
|
||||
};
|
||||
|
||||
const FEATURE_BADGE_VARIANT_CLASS: Record<
|
||||
NonNullable<MenuFeatureBadgeProps["variant"]>,
|
||||
string
|
||||
> = {
|
||||
cloud: "text-black",
|
||||
new: "bg-emerald-500 text-white",
|
||||
};
|
||||
|
||||
const FEATURE_BADGE_SIZE_CLASS: Record<
|
||||
NonNullable<MenuFeatureBadgeProps["size"]>,
|
||||
string
|
||||
> = {
|
||||
default: "h-6 rounded-lg px-2 text-xs leading-5",
|
||||
sm: "h-5 rounded-md px-1.5 text-[10px] leading-4",
|
||||
};
|
||||
|
||||
export const MenuFeatureBadge = ({
|
||||
label,
|
||||
variant = "cloud",
|
||||
size = "default",
|
||||
className,
|
||||
}: MenuFeatureBadgeProps) => (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex shrink-0 items-center justify-center font-bold whitespace-nowrap",
|
||||
FEATURE_BADGE_VARIANT_CLASS[variant],
|
||||
FEATURE_BADGE_SIZE_CLASS[size],
|
||||
className,
|
||||
)}
|
||||
style={FEATURE_BADGE_STYLE[variant]}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
|
||||
export const CloudFeatureBadge = ({
|
||||
label = "Available in Prowler Cloud",
|
||||
size,
|
||||
className,
|
||||
}: Omit<MenuFeatureBadgeProps, "variant">) => (
|
||||
<MenuFeatureBadge
|
||||
label={label}
|
||||
variant="cloud"
|
||||
size={size}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
|
||||
interface CloudFeatureBadgeLinkProps
|
||||
extends Omit<MenuFeatureBadgeProps, "variant"> {
|
||||
href?: string;
|
||||
}
|
||||
|
||||
export const CloudFeatureBadgeLink = ({
|
||||
href = "https://prowler.com/pricing",
|
||||
label,
|
||||
size,
|
||||
className,
|
||||
}: CloudFeatureBadgeLinkProps) => (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="shrink-0 whitespace-nowrap transition-opacity hover:opacity-90"
|
||||
>
|
||||
<CloudFeatureBadge label={label} size={size} className={className} />
|
||||
</a>
|
||||
);
|
||||
@@ -0,0 +1,136 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { useCloudUpgradeStore } from "@/store/cloud-upgrade/store";
|
||||
import { CLOUD_UPGRADE_FEATURE } from "@/types/cloud-upgrade";
|
||||
|
||||
import { CloudUpgradeModal } from "./cloud-upgrade-modal";
|
||||
|
||||
describe("CloudUpgradeModal", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.unstubAllEnvs();
|
||||
useCloudUpgradeStore.getState().closeCloudUpgrade();
|
||||
});
|
||||
|
||||
it("renders the active contextual upgrade in Local Server", async () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
useCloudUpgradeStore
|
||||
.getState()
|
||||
.openCloudUpgrade(CLOUD_UPGRADE_FEATURE.ALERTS);
|
||||
|
||||
// When
|
||||
render(<CloudUpgradeModal />);
|
||||
|
||||
// Then
|
||||
expect(
|
||||
await screen.findByRole("dialog", { name: "Turn Findings into Alerts" }),
|
||||
).toBeVisible();
|
||||
expect(screen.getByText("Available in Prowler Cloud")).toBeVisible();
|
||||
expect(
|
||||
screen.getByRole("link", { name: "Create Alerts in Prowler Cloud" }),
|
||||
).toHaveAttribute(
|
||||
"href",
|
||||
"https://cloud.prowler.com/sign-up?utm_source=prowler-local-server&utm_content=alerts",
|
||||
);
|
||||
expect(
|
||||
screen.getByRole("link", { name: "View Plans & Pricing" }),
|
||||
).toHaveAttribute(
|
||||
"href",
|
||||
"https://prowler.com/pricing?utm_source=prowler-local-server&utm_content=alerts",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the standard equal-width CTA layout", async () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
useCloudUpgradeStore
|
||||
.getState()
|
||||
.openCloudUpgrade(CLOUD_UPGRADE_FEATURE.AWS_ORGANIZATIONS);
|
||||
|
||||
// When
|
||||
render(<CloudUpgradeModal />);
|
||||
|
||||
// Then
|
||||
const dialog = await screen.findByRole("dialog", {
|
||||
name: "Add Your Entire AWS Organization",
|
||||
});
|
||||
const primaryCta = screen.getByRole("link", {
|
||||
name: "Set Up AWS Organizations in Prowler Cloud",
|
||||
});
|
||||
const secondaryCta = screen.getByRole("link", {
|
||||
name: "View Plans & Pricing",
|
||||
});
|
||||
|
||||
expect(dialog).toHaveClass("sm:max-w-2xl");
|
||||
expect(primaryCta.parentElement).toHaveClass("gap-3", "md:flex-row");
|
||||
expect(primaryCta).toHaveClass(
|
||||
"h-auto",
|
||||
"min-h-9",
|
||||
"whitespace-normal",
|
||||
"md:flex-1",
|
||||
);
|
||||
expect(secondaryCta).toHaveClass(
|
||||
"h-auto",
|
||||
"min-h-9",
|
||||
"whitespace-normal",
|
||||
"md:flex-1",
|
||||
);
|
||||
expect(primaryCta.querySelector(".truncate")).not.toBeInTheDocument();
|
||||
expect(primaryCta).toHaveAttribute(
|
||||
"href",
|
||||
"https://cloud.prowler.com/sign-up?utm_source=prowler-local-server&utm_content=organization",
|
||||
);
|
||||
});
|
||||
|
||||
it("closes the active upgrade and returns focus to its trigger", async () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
useCloudUpgradeStore
|
||||
.getState()
|
||||
.openCloudUpgrade(CLOUD_UPGRADE_FEATURE.GENERAL)
|
||||
}
|
||||
>
|
||||
Explore Prowler Cloud
|
||||
</button>
|
||||
<CloudUpgradeModal />
|
||||
</>,
|
||||
);
|
||||
|
||||
const trigger = screen.getByRole("button", {
|
||||
name: "Explore Prowler Cloud",
|
||||
});
|
||||
await user.click(trigger);
|
||||
|
||||
// When
|
||||
await user.click(screen.getByRole("button", { name: "Close" }));
|
||||
|
||||
// Then
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
expect(trigger).toHaveFocus();
|
||||
expect(useCloudUpgradeStore.getState().activeFeature).toBeNull();
|
||||
});
|
||||
|
||||
it("does not render upgrade UI in Prowler Cloud", () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
useCloudUpgradeStore
|
||||
.getState()
|
||||
.openCloudUpgrade(CLOUD_UPGRADE_FEATURE.ALERTS);
|
||||
|
||||
// When
|
||||
render(<CloudUpgradeModal />);
|
||||
|
||||
// Then
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
"use client";
|
||||
|
||||
import { Check, Cloud } from "lucide-react";
|
||||
|
||||
import { Badge } from "@/components/shadcn/badge/badge";
|
||||
import { Button } from "@/components/shadcn/button/button";
|
||||
import { Modal } from "@/components/shadcn/modal";
|
||||
import {
|
||||
CLOUD_UPGRADE_CONTENT,
|
||||
CLOUD_UPGRADE_FOOTER_NOTE,
|
||||
CLOUD_UPGRADE_SECONDARY_CTA,
|
||||
getCloudUpgradeCompareUrl,
|
||||
getCloudUpgradePrimaryUrl,
|
||||
} from "@/lib/cloud-upgrade";
|
||||
import { isCloud } from "@/lib/shared/env";
|
||||
import { useCloudUpgradeStore } from "@/store";
|
||||
import { CLOUD_UPGRADE_FEATURE } from "@/types/cloud-upgrade";
|
||||
|
||||
const allowInitialAutoFocus = () => {};
|
||||
|
||||
export const CloudUpgradeModal = () => {
|
||||
const activeFeature = useCloudUpgradeStore((state) => state.activeFeature);
|
||||
const closeCloudUpgrade = useCloudUpgradeStore(
|
||||
(state) => state.closeCloudUpgrade,
|
||||
);
|
||||
const returnFocusElement = useCloudUpgradeStore(
|
||||
(state) => state.returnFocusElement,
|
||||
);
|
||||
|
||||
if (isCloud()) return null;
|
||||
|
||||
const feature = activeFeature ?? CLOUD_UPGRADE_FEATURE.GENERAL;
|
||||
const content = CLOUD_UPGRADE_CONTENT[feature];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={activeFeature !== null}
|
||||
onOpenChange={(open) => !open && closeCloudUpgrade()}
|
||||
onOpenAutoFocus={allowInitialAutoFocus}
|
||||
onCloseAutoFocus={(event) => {
|
||||
event.preventDefault();
|
||||
returnFocusElement?.focus();
|
||||
}}
|
||||
title={content.title}
|
||||
description={content.description}
|
||||
size="2xl"
|
||||
>
|
||||
<div className="min-w-0 space-y-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-bg-neutral-tertiary text-text-neutral-primary flex size-10 items-center justify-center rounded-xl">
|
||||
<Cloud aria-hidden="true" className="size-5" />
|
||||
</div>
|
||||
<Badge variant="cloud">Available in Prowler Cloud</Badge>
|
||||
</div>
|
||||
|
||||
<ul className="space-y-3">
|
||||
{content.benefits.map((benefit) => (
|
||||
<li
|
||||
key={benefit}
|
||||
className="text-text-neutral-secondary flex items-start gap-3 text-sm"
|
||||
>
|
||||
<Check
|
||||
aria-hidden="true"
|
||||
className="text-text-success mt-0.5 size-4"
|
||||
/>
|
||||
<span>{benefit}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="flex flex-col gap-3 md:flex-row">
|
||||
<Button
|
||||
asChild
|
||||
className="h-auto min-h-9 w-full min-w-0 shrink whitespace-normal md:flex-1"
|
||||
>
|
||||
<a
|
||||
href={getCloudUpgradePrimaryUrl(feature)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title={content.primaryCta}
|
||||
>
|
||||
{content.primaryCta}
|
||||
</a>
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
className="h-auto min-h-9 w-full min-w-0 shrink whitespace-normal md:flex-1"
|
||||
>
|
||||
<a
|
||||
href={getCloudUpgradeCompareUrl(feature)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title={CLOUD_UPGRADE_SECONDARY_CTA}
|
||||
>
|
||||
{CLOUD_UPGRADE_SECONDARY_CTA}
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p className="text-text-neutral-tertiary text-center text-xs">
|
||||
{CLOUD_UPGRADE_FOOTER_NOTE}
|
||||
</p>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user