merge master into grouped Jira UI

This commit is contained in:
Hugo P.Brito committed 2026-07-22 12:12:46 +01:00
commit 33ff2a2a44
316 files changed
+10112 -4023

No files matched your search

+1 -1
View File
@@ -17,8 +17,8 @@ import {
Tooltip,
TooltipContent,
TooltipTrigger,
useToast,
} from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { CustomInput } from "@/components/shadcn/custom";
import { Form } from "@/components/shadcn/form";
import { getSafeCallbackPath } from "@/lib/auth-callback-url";
+1 -2
View File
@@ -15,8 +15,7 @@ import { AuthFooterLink } from "@/components/auth/oss/auth-footer-link";
import { AuthLayout } from "@/components/auth/oss/auth-layout";
import { PasswordRequirementsMessage } from "@/components/auth/oss/password-validator";
import { SocialButtons } from "@/components/auth/oss/social-buttons";
import { Button, Checkbox } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { Button, Checkbox, useToast } from "@/components/shadcn";
import { CustomInput } from "@/components/shadcn/custom";
import { CustomLink } from "@/components/shadcn/custom/custom-link";
import {
@@ -1,8 +1,15 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { SocialButtons } from "./social-buttons";
// Stub Iconify: the real <Icon> fetches icon data over the network and its
// retry timers can fire after the jsdom environment is torn down, crashing the
// worker with "window is not defined".
vi.mock("@iconify/react", () => ({
Icon: ({ icon }: { icon: string }) => <span aria-label={icon} />,
}));
describe("SocialButtons", () => {
it("renders icon-only provider links that keep their accessible names", () => {
render(
@@ -2,8 +2,7 @@
import { useRef, useState } from "react";
import { Button } from "@/components/shadcn";
import { Accordion, AccordionItemProps } from "@/components/shadcn";
import { Button, Accordion, AccordionItemProps } from "@/components/shadcn";
import { Card } from "@/components/shadcn/card/card";
export const ClientAccordionWrapper = ({
@@ -19,6 +19,7 @@ import {
import { ScanEntity } from "@/types/scans";
import { getComplianceIcon } from "../icons";
import { ComplianceDownloadContainer } from "./compliance-download-container";
interface ComplianceCardProps {
@@ -4,6 +4,17 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import { useCloudUpgradeStore } from "@/store/cloud-upgrade/store";
import { CLOUD_UPGRADE_FEATURE } from "@/types/cloud-upgrade";
import {
FINDING_TRIAGE_DISABLED_REASON,
FINDING_TRIAGE_STATUS,
type FindingTriageSummary,
} from "@/types/findings-triage";
import {
DataTableRowActions,
type FindingRowData,
} from "./data-table-row-actions";
import { FindingsSelectionContext } from "./findings-selection-context";
const { MuteFindingsModalMock, isGroupedJiraDispatchEnabledMock } = vi.hoisted(
() => ({
@@ -97,18 +108,6 @@ vi.mock("./finding-note-modal", () => ({
) : null,
}));
import {
FINDING_TRIAGE_DISABLED_REASON,
FINDING_TRIAGE_STATUS,
type FindingTriageSummary,
} from "@/types/findings-triage";
import {
DataTableRowActions,
type FindingRowData,
} from "./data-table-row-actions";
import { FindingsSelectionContext } from "./findings-selection-context";
function deferredPromise<T>() {
let resolve!: (value: T) => void;
let reject!: (reason?: unknown) => void;
@@ -22,8 +22,9 @@ import {
TableHead,
TableHeader,
TableRow,
SeverityBadge,
StatusFindingBadge,
} from "@/components/shadcn/table";
import { SeverityBadge, StatusFindingBadge } from "@/components/shadcn/table";
import { useFindingGroupResourceState } from "@/hooks/use-finding-group-resource-state";
import { cn, hasHistoricalFindingFilter } from "@/lib";
import { isGroupedJiraDispatchEnabled } from "@/lib/deployment";
@@ -37,6 +38,7 @@ import { FindingGroupRow } from "@/types";
import { JIRA_DISPATCH_MODE, JIRA_DISPATCH_TARGET } from "@/types/integrations";
import { FloatingMuteButton } from "../floating-mute-button";
import { getColumnFindingResources } from "./column-finding-resources";
import { FindingsSelectionContext } from "./findings-selection-context";
import { ImpactedResourcesCell } from "./impacted-resources-cell";
@@ -21,6 +21,7 @@ import { FindingGroupRow, MetaDataProps } from "@/types";
import { JIRA_DISPATCH_MODE, JIRA_DISPATCH_TARGET } from "@/types/integrations";
import { FloatingMuteButton } from "../floating-mute-button";
import { getColumnFindingGroups } from "./column-finding-groups";
import { canMuteFindingGroup } from "./finding-group-selection";
import { FindingsSelectionContext } from "./findings-selection-context";
@@ -71,8 +71,7 @@ import {
type QueryEditorLanguage,
} from "@/components/shared/query-code-editor";
import { ResourceMetadataPanel } from "@/components/shared/resource-metadata-panel";
import { getFailingForLabel } from "@/lib/date-utils";
import { formatDuration } from "@/lib/date-utils";
import { getFailingForLabel, formatDuration } from "@/lib/date-utils";
import { shouldRefreshAfterTriageUpdate } from "@/lib/finding-triage";
import { createJiraTargetSelection } from "@/lib/jira-dispatch-selection";
import { buildFindingAnalysisPrompt } from "@/lib/lighthouse/prompts";
@@ -91,6 +90,7 @@ import {
FindingTriageStatusCell,
} from "../finding-triage-cells";
import { DeltaValues, NotificationIndicator } from "../notification-indicator";
import { ResourceDetailSkeleton } from "./resource-detail-skeleton";
import type { CheckMeta } from "./use-resource-detail-drawer";
+42
View File
@@ -0,0 +1,42 @@
import { describe, expect, it } from "vitest";
import { formatYAxisTick } from "./line-chart.utils";
describe("formatYAxisTick", () => {
describe("when findings counts are large", () => {
it("should compact six-digit values so Y-axis labels do not overflow", () => {
// Given
const tickValue = 150000;
// When
const formattedValue = formatYAxisTick(tickValue);
// Then
expect(formattedValue).toBe("150K");
});
it("should compact million-scale values", () => {
// Given
const tickValue = 1200000;
// When
const formattedValue = formatYAxisTick(tickValue);
// Then
expect(formattedValue).toBe("1.2M");
});
});
describe("when findings counts are small", () => {
it("should keep values below 1000 readable without compact notation", () => {
// Given
const tickValue = 999;
// When
const formattedValue = formatYAxisTick(tickValue);
// Then
expect(formattedValue).toBe("999");
});
});
});
+3
View File
@@ -17,6 +17,7 @@ import {
ChartTooltip,
} from "@/components/shadcn/chart/Chart";
import { formatYAxisTick } from "./line-chart.utils";
import { AlertPill } from "./shared/alert-pill";
import { ChartLegend } from "./shared/chart-legend";
import { CustomActiveDot, PointClickData } from "./shared/custom-active-dot";
@@ -222,6 +223,8 @@ export function LineChart({
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={formatYAxisTick}
width={56}
padding={{ top: 20 }}
tick={{
fill: "var(--color-text-neutral-secondary)",
+8
View File
@@ -0,0 +1,8 @@
const Y_AXIS_TICK_FORMATTER = new Intl.NumberFormat("en-US", {
notation: "compact",
maximumFractionDigits: 1,
});
export function formatYAxisTick(value: number) {
return Y_AXIS_TICK_FORMATTER.format(value);
}
@@ -11,7 +11,7 @@ describe("ProwlerBrand", () => {
it("should render the Local Server lockups outside Cloud", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
// When
render(<ProwlerBrand />);
@@ -30,7 +30,7 @@ describe("ProwlerBrand", () => {
it("should render the Prowler Cloud lockups in Cloud", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
// When
render(<ProwlerBrand />);
@@ -4,11 +4,9 @@ import { SettingsIcon } from "lucide-react";
import Link from "next/link";
import { JiraIcon } from "@/components/icons/services/IconServices";
import { Button } from "@/components/shadcn";
import { Button, Card, CardContent, CardHeader } from "@/components/shadcn";
import { CustomLink } from "@/components/shadcn/custom/custom-link";
import { Card, CardContent, CardHeader } from "../../shadcn";
export const JiraIntegrationCard = () => {
return (
<Card variant="base" padding="lg">
@@ -15,15 +15,19 @@ import {
IntegrationCardHeader,
IntegrationSkeleton,
} from "@/components/integrations/shared";
import { Button } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import {
Button,
useToast,
Card,
CardContent,
CardHeader,
} from "@/components/shadcn";
import { Modal } from "@/components/shadcn/modal";
import { DataTablePagination } from "@/components/shadcn/table/data-table-pagination";
import { triggerTestConnectionWithDelay } from "@/lib/integrations/test-connection-helper";
import { MetaDataProps } from "@/types";
import { IntegrationProps } from "@/types/integrations";
import { Card, CardContent, CardHeader } from "../../shadcn";
import { JiraIntegrationForm } from "./jira-integration-form";
interface JiraIntegrationsManagerProps {
@@ -4,11 +4,9 @@ import { SettingsIcon } from "lucide-react";
import Link from "next/link";
import { AmazonS3Icon } from "@/components/icons/services/IconServices";
import { Button } from "@/components/shadcn";
import { Button, Card, CardContent, CardHeader } from "@/components/shadcn";
import { CustomLink } from "@/components/shadcn/custom/custom-link";
import { Card, CardContent, CardHeader } from "../../shadcn";
export const S3IntegrationCard = () => {
return (
<Card variant="base" padding="lg">
@@ -13,8 +13,7 @@ import {
ProviderTypeIcon,
} from "@/components/icons/providers-badge/provider-type-icon";
import { AWSRoleCredentialsForm } from "@/components/providers/workflow/forms/select-credentials-type/aws/credentials-type/aws-role-credentials-form";
import { Separator } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { Separator, useToast } from "@/components/shadcn";
import { CustomInput } from "@/components/shadcn/custom";
import { CustomLink } from "@/components/shadcn/custom/custom-link";
import {
@@ -26,6 +25,7 @@ import {
import { FormButtons } from "@/components/shadcn/form/form-buttons";
import { EnhancedMultiSelect } from "@/components/shadcn/select/enhanced-multi-select";
import { getAWSCredentialsTemplateLinks } from "@/lib";
import { isCloud } from "@/lib/shared/env";
import type { AWSCredentialsRole } from "@/types";
import type { IntegrationProps } from "@/types/integrations";
import {
@@ -78,10 +78,9 @@ export const S3IntegrationForm = ({
const isEditingConfig = editMode === "configuration";
const isEditingCredentials = editMode === "credentials";
const defaultCredentialsType =
process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true"
? "aws-sdk-default"
: "access-secret-key";
const defaultCredentialsType = isCloud()
? "aws-sdk-default"
: "access-secret-key";
const form = useForm({
resolver: zodResolver(
@@ -15,8 +15,13 @@ import {
IntegrationCardHeader,
IntegrationSkeleton,
} from "@/components/integrations/shared";
import { Button } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import {
Button,
useToast,
Card,
CardContent,
CardHeader,
} from "@/components/shadcn";
import { Modal } from "@/components/shadcn/modal";
import { DataTablePagination } from "@/components/shadcn/table/data-table-pagination";
import { triggerTestConnectionWithDelay } from "@/lib/integrations/test-connection-helper";
@@ -24,7 +29,6 @@ import { MetaDataProps } from "@/types";
import { IntegrationProps } from "@/types/integrations";
import { ProviderProps } from "@/types/providers";
import { Card, CardContent, CardHeader } from "../../shadcn";
import { S3IntegrationForm } from "./s3-integration-form";
interface S3IntegrationsManagerProps {
@@ -12,8 +12,13 @@ import { z } from "zod";
import { createSamlConfig, updateSamlConfig } from "@/actions/integrations";
import { AddIcon } from "@/components/icons";
import { Button, Card, CardContent, CardHeader } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import {
Button,
Card,
CardContent,
CardHeader,
useToast,
} from "@/components/shadcn";
import { CodeSnippet } from "@/components/shadcn/code-snippet/code-snippet";
import { CustomServerInput } from "@/components/shadcn/custom";
import { CustomLink } from "@/components/shadcn/custom/custom-link";
@@ -4,11 +4,9 @@ import { SettingsIcon } from "lucide-react";
import Link from "next/link";
import { AWSSecurityHubIcon } from "@/components/icons/services/IconServices";
import { Button } from "@/components/shadcn";
import { Button, Card, CardContent, CardHeader } from "@/components/shadcn";
import { CustomLink } from "@/components/shadcn/custom/custom-link";
import { Card, CardContent, CardHeader } from "../../shadcn";
export const SecurityHubIntegrationCard = () => {
return (
<Card variant="base" padding="lg">
@@ -12,8 +12,7 @@ import {
ProviderTypeIcon,
} from "@/components/icons/providers-badge/provider-type-icon";
import { AWSRoleCredentialsForm } from "@/components/providers/workflow/forms/select-credentials-type/aws/credentials-type/aws-role-credentials-form";
import { Checkbox, Separator } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { Checkbox, Separator, useToast } from "@/components/shadcn";
import { CustomLink } from "@/components/shadcn/custom/custom-link";
import {
Form,
@@ -15,8 +15,14 @@ import {
IntegrationCardHeader,
IntegrationSkeleton,
} from "@/components/integrations/shared";
import { Badge, Button } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import {
Badge,
Button,
useToast,
Card,
CardContent,
CardHeader,
} from "@/components/shadcn";
import { Modal } from "@/components/shadcn/modal";
import { DataTablePagination } from "@/components/shadcn/table/data-table-pagination";
import { triggerTestConnectionWithDelay } from "@/lib/integrations/test-connection-helper";
@@ -24,7 +30,6 @@ import { MetaDataProps } from "@/types";
import { IntegrationProps } from "@/types/integrations";
import { ProviderProps } from "@/types/providers";
import { Card, CardContent, CardHeader } from "../../shadcn";
import { SecurityHubIntegrationForm } from "./security-hub-integration-form";
interface SecurityHubIntegrationsManagerProps {
@@ -3,11 +3,9 @@
import { ExternalLinkIcon, LucideIcon } from "lucide-react";
import Link from "next/link";
import { Button } from "@/components/shadcn";
import { Button, Card, CardContent, CardHeader } from "@/components/shadcn";
import { CustomLink } from "@/components/shadcn/custom/custom-link";
import { Card, CardContent, CardHeader } from "../../shadcn";
interface LinkCardProps {
icon: LucideIcon;
title: string;
@@ -7,8 +7,7 @@ import * as z from "zod";
import { revokeInvite } from "@/actions/invitations/invitation";
import { DeleteIcon } from "@/components/icons";
import { Button } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { Button, useToast } from "@/components/shadcn";
import { Form } from "@/components/shadcn/form";
const formSchema = z.object({
@@ -5,7 +5,7 @@ import { Controller, useForm } from "react-hook-form";
import * as z from "zod";
import { updateInvite } from "@/actions/invitations/invitation";
import { useToast } from "@/components/shadcn";
import { useToast, Card, CardContent } from "@/components/shadcn";
import { CustomInput } from "@/components/shadcn/custom";
import { Form, FormButtons } from "@/components/shadcn/form";
import {
@@ -17,8 +17,6 @@ import {
} from "@/components/shadcn/select/select";
import { editInviteFormSchema } from "@/types";
import { Card, CardContent } from "../../shadcn";
export const EditForm = ({
invitationId,
invitationEmail,
@@ -7,8 +7,7 @@ import { Controller, useForm } from "react-hook-form";
import * as z from "zod";
import { sendInvite } from "@/actions/invitations/invitation";
import { Button } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { Button, useToast } from "@/components/shadcn";
import { CustomInput } from "@/components/shadcn/custom";
import { Form } from "@/components/shadcn/form";
import {
@@ -2,19 +2,18 @@
import { useControlledState } from "@react-stately/utils";
import { domAnimation, LazyMotion, m } from "framer-motion";
import type { ComponentProps } from "react";
import React from "react";
import { forwardRef, useMemo } from "react";
import type { ComponentProps, HTMLAttributes, ReactNode } from "react";
import { cn } from "@/lib/utils";
export type VerticalStepProps = {
className?: string;
description?: React.ReactNode;
title?: React.ReactNode;
description?: ReactNode;
title?: ReactNode;
};
export interface VerticalStepsProps
extends React.HTMLAttributes<HTMLButtonElement> {
export interface VerticalStepsProps extends HTMLAttributes<HTMLButtonElement> {
/**
* An array of steps.
*
@@ -89,10 +88,7 @@ function CheckIcon(props: ComponentProps<"svg">) {
);
}
export const VerticalSteps = React.forwardRef<
HTMLButtonElement,
VerticalStepsProps
>(
export const VerticalSteps = forwardRef<HTMLButtonElement, VerticalStepsProps>(
(
{
color = "primary",
@@ -113,7 +109,7 @@ export const VerticalSteps = React.forwardRef<
onStepChange,
);
const colors = React.useMemo(() => {
const colors = useMemo(() => {
let userColor;
let fgColor;
@@ -65,7 +65,7 @@ describe("AppSidebarContent", () => {
it("shares the brand, Launch Scan action and Local Server Cloud affordances", async () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
vi.stubEnv("NEXT_PUBLIC_PROWLER_RELEASE_VERSION", "5.8.0");
const user = userEvent.setup();
@@ -91,7 +91,7 @@ describe("AppSidebarContent", () => {
it("keeps the existing Lighthouse chat sidebar in Cloud Chat mode", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
useAppSidebarMode.setState({ mode: APP_SIDEBAR_MODE.CHAT });
// When
@@ -109,7 +109,7 @@ describe("AppSidebarContent", () => {
it("opens the current scan modal instead of navigating from the scans route", async () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
pathnameValue.current = "/scans";
const user = userEvent.setup();
@@ -24,15 +24,20 @@ interface AppSidebarContentProps {
export function AppSidebarContent({ onSelect }: AppSidebarContentProps) {
const pathname = usePathname();
const { permissions } = useAuth();
const { apiDocsUrl } = useRuntimeConfig();
const { apiDocsUrl, cloudBillingEnabled } = useRuntimeConfig();
const mode = useAppSidebarMode((state) => state.mode);
const isCloudEnvironment = isCloud();
const sections = getNavigationConfig({ pathname, apiDocsUrl, permissions });
const sections = getNavigationConfig({
pathname,
apiDocsUrl,
cloudBillingEnabled,
permissions,
});
const showChat = isCloudEnvironment && mode === APP_SIDEBAR_MODE.CHAT;
return (
<div className="relative flex h-full min-h-0 w-full flex-col overflow-hidden">
<div className="shrink-0 px-5 pt-6 pb-7">
<div className="shrink-0 px-5 pt-8 pb-7">
<Link
href="/"
aria-label="Prowler home"
@@ -31,7 +31,7 @@ describe("getNavigationConfig", () => {
it("groups the Local Server navigation without losing available features", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
// When
const sections = getNavigationConfig({
@@ -71,7 +71,7 @@ describe("getNavigationConfig", () => {
it("models Local Server Cloud features as contextual upgrade actions", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
// When
const children = getConfigurationChildren();
@@ -108,7 +108,7 @@ describe("getNavigationConfig", () => {
it("uses Cloud destinations and current New badges", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
// When
const sections = getNavigationConfig({
@@ -148,7 +148,7 @@ describe("getNavigationConfig", () => {
it("keeps the Cloud Billing destination for users with billing permission", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
const permissions = {
manage_billing: true,
} as RolePermissionAttributes;
@@ -157,6 +157,7 @@ describe("getNavigationConfig", () => {
const billing = getNavigationConfig({
pathname: "/billing",
apiDocsUrl: null,
cloudBillingEnabled: true,
permissions,
})
.flatMap((section) => section.items)
@@ -177,29 +178,40 @@ describe("getNavigationConfig", () => {
const permissions = {
manage_billing: false,
} as RolePermissionAttributes;
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
// When
const cloudItems = getNavigationConfig({
pathname: "/",
apiDocsUrl: null,
cloudBillingEnabled: true,
permissions,
}).flatMap((section) => section.items);
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
const enterpriseItems = getNavigationConfig({
pathname: "/",
apiDocsUrl: null,
cloudBillingEnabled: false,
permissions: { ...permissions, manage_billing: true },
}).flatMap((section) => section.items);
vi.stubEnv("UI_CLOUD_ENABLED", "false");
const localItems = getNavigationConfig({
pathname: "/",
apiDocsUrl: null,
cloudBillingEnabled: true,
permissions: { ...permissions, manage_billing: true },
}).flatMap((section) => section.items);
// Then
expect(cloudItems.find((item) => item.label === "Billing")).toBeUndefined();
expect(
enterpriseItems.find((item) => item.label === "Billing"),
).toBeUndefined();
expect(localItems.find((item) => item.label === "Billing")).toBeUndefined();
});
it("keeps environment-specific API documentation destinations", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
// When
const localApiReference = getNavigationConfig({
@@ -209,7 +221,7 @@ describe("getNavigationConfig", () => {
.flatMap((section) => section.items)
.find((item) => item.label === "API Reference");
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
const cloudApiReference = getNavigationConfig({
pathname: "/",
apiDocsUrl: "https://ignored.example/docs",
@@ -228,7 +240,7 @@ describe("getNavigationConfig", () => {
it("omits the Local Server API reference when no URL is configured", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
// When
const items = getNavigationConfig({
@@ -244,7 +256,7 @@ describe("getNavigationConfig", () => {
it("filters navigation by required permission after visible copy changes", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
const sections = getNavigationConfig({
pathname: "/integrations",
apiDocsUrl: null,
@@ -282,7 +294,7 @@ describe("getNavigationConfig", () => {
it("keeps navigation when the required permission is granted", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
const permissions = {
manage_integrations: true,
} as RolePermissionAttributes;
@@ -308,7 +320,7 @@ describe("getNavigationConfig", () => {
it("matches complete route segments without stealing nested settings routes", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
// When
const scanDetails = getNavigationConfig({
@@ -31,6 +31,7 @@ import {
interface NavigationConfigOptions {
pathname: string;
apiDocsUrl?: string | null;
cloudBillingEnabled?: boolean;
permissions?: RolePermissionAttributes;
}
@@ -106,6 +107,7 @@ export function filterNavigationByPermissions(
export function getNavigationConfig({
pathname,
apiDocsUrl = null,
cloudBillingEnabled = false,
permissions,
}: NavigationConfigOptions): NavigationSection[] {
const isCloudEnvironment = isCloud();
@@ -265,7 +267,7 @@ export function getNavigationConfig({
},
],
},
...(isCloudEnvironment
...(isCloudEnvironment && cloudBillingEnabled
? [
{
kind: NAVIGATION_ITEM_KIND.LINK,
@@ -67,7 +67,7 @@ describe("NavbarClient", () => {
navigationMocks.searchParams = new URLSearchParams();
window.localStorage.clear();
// Replay icon is Cloud-only.
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
// Default: the current route's content has loaded, so the icon is enabled.
usePageReadyStore.setState({ readyPath: "/findings" });
useSidePanelStore.setState({
@@ -223,7 +223,7 @@ describe("NavbarClient", () => {
});
it("hides the replay icon entirely in self-hosted (OSS) deployments", () => {
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
render(
<NavbarClient
@@ -280,7 +280,7 @@ describe("NavbarClient", () => {
it("hides the Lighthouse AI side-panel trigger in self-hosted (OSS) deployments", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
// When
render(<NavbarClient title="Findings" />);
+1 -1
View File
@@ -34,8 +34,8 @@ import {
CardHeader,
CardTitle,
Combobox,
useToast,
} from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { CustomLink } from "@/components/shadcn/custom/custom-link";
import { useMountEffect } from "@/hooks/use-mount-effect";
import type { LighthouseProvider } from "@/types/lighthouse-v1";
@@ -17,8 +17,8 @@ import {
CardContent,
CardHeader,
CardTitle,
useToast,
} from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { CustomTextarea } from "@/components/shadcn/custom";
import { Form } from "@/components/shadcn/form";
@@ -5,8 +5,7 @@ import { Controller, useForm } from "react-hook-form";
import * as z from "zod";
import { createProviderGroup } from "@/actions/manage-groups";
import { Button, Separator } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { Button, Separator, useToast } from "@/components/shadcn";
import { CustomInput } from "@/components/shadcn/custom";
import { Form } from "@/components/shadcn/form";
import { EnhancedMultiSelect } from "@/components/shadcn/select/enhanced-multi-select";
@@ -8,8 +8,7 @@ import * as z from "zod";
import { deleteProviderGroup } from "@/actions/manage-groups/manage-groups";
import { DeleteIcon } from "@/components/icons";
import { Button } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { Button, useToast } from "@/components/shadcn";
import { Form } from "@/components/shadcn/form";
const formSchema = z.object({
@@ -7,8 +7,7 @@ import { Controller, useForm } from "react-hook-form";
import * as z from "zod";
import { updateProviderGroup } from "@/actions/manage-groups/manage-groups";
import { Button, Separator } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { Button, Separator, useToast } from "@/components/shadcn";
import { CustomInput } from "@/components/shadcn/custom";
import { Form } from "@/components/shadcn/form";
import { EnhancedMultiSelect } from "@/components/shadcn/select/enhanced-multi-select";
@@ -84,7 +84,7 @@ describe("OnboardingTrigger", () => {
useDriverTourMock.mockClear();
capturedOnClosed = undefined;
// Trigger only resolves in cloud.
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
searchParamsValue = new URLSearchParams();
sliceState = {
active: false,
@@ -196,7 +196,7 @@ describe("OnboardingTrigger", () => {
describe("in self-hosted (OSS) deployments", () => {
it("renders null and never starts the tour, even with a matching param", async () => {
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
searchParamsValue = new URLSearchParams("onboarding=add-provider");
const { container } = render(
@@ -208,7 +208,7 @@ describe("OnboardingTrigger", () => {
});
it("ignores an active sequence slice in OSS", async () => {
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
setSlice({
active: true,
currentFlowId: "add-provider",
@@ -7,8 +7,7 @@ import * as z from "zod";
import { deleteProvider } from "@/actions/providers";
import { DeleteIcon } from "@/components/icons";
import { Button } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { Button, useToast } from "@/components/shadcn";
import { Form } from "@/components/shadcn/form";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
@@ -7,8 +7,7 @@ import {
deleteOrganizationalUnit,
} from "@/actions/organizations/organizations";
import { DeleteIcon } from "@/components/icons";
import { Button } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { Button, useToast } from "@/components/shadcn";
import {
PROVIDERS_GROUP_KIND,
ProvidersGroupKind,
@@ -4,8 +4,7 @@ import type { Dispatch, FormEvent, SetStateAction } from "react";
import { useState } from "react";
import { SaveIcon } from "@/components/icons";
import { Button } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { Button, useToast } from "@/components/shadcn";
import { Input } from "@/components/shadcn/input/input";
interface EditNameFormProps {
@@ -15,7 +15,7 @@ describe("AwsMethodSelector", () => {
it("opens the AWS Organizations upgrade in Local Server", async () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
const user = userEvent.setup();
const onSelectOrganizations = vi.fn();
@@ -26,6 +26,7 @@ import {
pollConnectionTask,
runWithConcurrencyLimit,
} from "../org-account-selection.utils";
import { extractErrorMessage } from "./error-utils";
interface SelectionState {
@@ -115,7 +115,7 @@ describe("OrgSetupForm", () => {
expect(params.get("param_DeployFromDelegatedAdmin")).toBe("true");
expect(
screen.getByLabelText("Delegated Administrator Account Role ARN"),
screen.getByLabelText("Delegated Administrator Account IAM Role ARN"),
).toBeInTheDocument();
});
});
@@ -41,7 +41,7 @@ const orgSetupSchema = z.object({
roleArn: z
.string()
.trim()
.min(1, "Role ARN is required")
.min(1, "IAM Role ARN is required")
.regex(
/^arn:aws:iam::\d{12}:role\//,
"Must be a valid IAM Role ARN (e.g., arn:aws:iam::123456789012:role/ProwlerScan)",
@@ -400,7 +400,7 @@ export function OrgSetupForm({
<div className="flex flex-col gap-4">
<p className="text-text-neutral-primary text-sm leading-7 font-normal">
1) Choose the AWS <strong>Organizational Unit</strong> (or root)
to deploy to. Prowler creates the role in your deployment
to deploy to. Prowler creates the IAM Role in your deployment
account and rolls it out to every member account under this
target.
</p>
@@ -422,7 +422,7 @@ export function OrgSetupForm({
to the whole organization, or an <strong>OU ID</strong> (starts
with <code>ou-</code>) to target a specific unit.
</p>
<div className="flex items-start gap-4">
<div className="flex items-center gap-3">
<Controller
name="deployFromDelegatedAdmin"
control={control}
@@ -430,7 +430,7 @@ export function OrgSetupForm({
<>
<Checkbox
id="deployFromDelegatedAdmin"
className="mt-0.5"
size="sm"
checked={field.value}
onCheckedChange={(checked) =>
field.onChange(Boolean(checked))
@@ -438,10 +438,10 @@ export function OrgSetupForm({
/>
<label
htmlFor="deployFromDelegatedAdmin"
className="text-text-neutral-tertiary text-xs leading-5 font-normal"
className="text-text-neutral-secondary text-sm leading-5 font-medium"
>
I&apos;m deploying from a delegated administrator
account (not the Organization management account)
I&apos;m deploying from a Delegated Administrator
Account (not the Management Account)
</label>
</>
)}
@@ -454,12 +454,12 @@ export function OrgSetupForm({
<p className="text-text-neutral-primary text-sm leading-7 font-normal">
2) Create the CloudFormation Stack in your{" "}
<strong>{deploymentAccountName}</strong>. It deploys the
ProwlerScan role and a service-managed StackSet that rolls the
role out to your member accounts in one step.
ProwlerScan IAM Role and a service-managed StackSet that rolls
the IAM Role out to your member accounts in one step.
</p>
{orgQuickLink ? (
<Button
variant="outline"
variant="default"
size="xl"
className="w-full justify-start"
asChild
@@ -476,7 +476,7 @@ export function OrgSetupForm({
) : (
<Button
type="button"
variant="outline"
variant="default"
size="xl"
className="w-full justify-start"
disabled
@@ -486,7 +486,7 @@ export function OrgSetupForm({
</Button>
)}
{!isOrgUnitIdValid && (
<p className="text-text-neutral-tertiary text-xs leading-5">
<p className="text-text-error-primary text-xs leading-5">
Enter a valid Organizational Unit or Root ID above to enable
deployment.
</p>
@@ -496,15 +496,15 @@ export function OrgSetupForm({
{/* Step 3: Role ARN + confirm */}
<div className="flex flex-col gap-4">
<p className="text-text-neutral-primary text-sm leading-7 font-normal">
3) Paste the {deploymentAccountName} Role ARN and confirm the
deployment is complete.
3) Paste the {deploymentAccountName} IAM Role ARN and confirm
the deployment is complete.
</p>
</div>
<WizardInputField
control={control}
name="roleArn"
label={`${deploymentAccountLabel} Role ARN`}
label={`${deploymentAccountLabel} IAM Role ARN`}
labelPlacement="outside"
placeholder="e.g. arn:aws:iam::123456789012:role/ProwlerScan"
isRequired={false}
@@ -516,7 +516,7 @@ export function OrgSetupForm({
ARN
</p>
<div className="flex items-start gap-4">
<div className="flex items-center gap-3">
<Controller
name="stackSetDeployed"
control={control}
@@ -524,7 +524,7 @@ export function OrgSetupForm({
<>
<Checkbox
id="stackSetDeployed"
className="mt-0.5"
size="sm"
checked={field.value}
onCheckedChange={(checked) =>
field.onChange(Boolean(checked))
@@ -3,8 +3,7 @@
import { useState } from "react";
import { setScanConfigurationProviders } from "@/actions/scan-configurations";
import { Button } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { Button, useToast } from "@/components/shadcn";
import { CustomLink } from "@/components/shadcn/custom/custom-link";
import { Modal } from "@/components/shadcn/modal";
import {
@@ -31,6 +31,7 @@ import type {
} from "@/types/schedules";
import { LinkToScans } from "../link-to-scans";
import { DataTableRowActions } from "./data-table-row-actions";
interface GroupNameChipsProps {
@@ -393,7 +393,7 @@ describe("DataTableRowActions", () => {
it("opens Edit Scan Schedule for Prowler Cloud subscribed provider rows", async () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
const user = userEvent.setup();
render(
@@ -420,7 +420,7 @@ describe("DataTableRowActions", () => {
it("hides Edit Scan Schedule for manual-only Cloud provider rows", async () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
const user = userEvent.setup();
render(
@@ -445,7 +445,7 @@ describe("DataTableRowActions", () => {
it("hides Edit Scan Schedule for blocked Cloud provider rows", async () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
const user = userEvent.setup();
render(
@@ -470,7 +470,7 @@ describe("DataTableRowActions", () => {
it("opens scan config management with the precomputed current config id", async () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
const user = userEvent.setup();
render(
@@ -500,7 +500,7 @@ describe("DataTableRowActions", () => {
it("shows scan config management as unavailable when scan configs failed to load", async () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
const user = userEvent.setup();
render(
@@ -528,7 +528,7 @@ describe("DataTableRowActions", () => {
it("hides Edit Scan Configuration for dynamic providers in Prowler Cloud", async () => {
// Given a dynamic provider in a Cloud tenant with scan configs available.
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
const user = userEvent.setup();
render(
@@ -10,6 +10,7 @@ import {
} from "@/types/provider-wizard";
import type { ProviderWizardInitialData } from "../types";
import { useProviderWizardController } from "./use-provider-wizard-controller";
const { refreshMock, requestOpenOnWizardCloseMock } = vi.hoisted(() => ({
@@ -46,7 +47,7 @@ describe("useProviderWizardController", () => {
sessionStorage.clear();
localStorage.clear();
// Checkpoint is Cloud-only.
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
useProviderWizardStore.getState().reset();
useOrgSetupStore.getState().reset();
});
@@ -117,7 +118,7 @@ describe("useProviderWizardController", () => {
});
it("does not request the onboarding checkpoint in self-hosted (OSS) deployments", () => {
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
const onOpenChange = vi.fn();
const { result } = renderHook(() =>
useProviderWizardController({
@@ -22,6 +22,7 @@ import {
import { SelectViaGitHub } from "../../workflow/forms/select-credentials-type/github";
import { SelectViaM365 } from "../../workflow/forms/select-credentials-type/m365";
import { UpdateViaServiceAccountForm } from "../../workflow/forms/update-via-service-account-key-form";
import {
WIZARD_FOOTER_ACTION_TYPE,
WizardFooterConfig,
@@ -76,7 +76,7 @@ describe("LaunchStep", () => {
describe("Prowler OSS (non-Cloud)", () => {
beforeEach(() => {
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
scanOnDemandMock.mockResolvedValue({ data: { id: "scan-1" } });
});
@@ -174,7 +174,7 @@ describe("LaunchStep", () => {
describe("Prowler Cloud subscribed", () => {
beforeEach(() => {
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
updateScheduleMock.mockResolvedValue({ data: { id: "provider-1" } });
});
@@ -370,7 +370,7 @@ describe("LaunchStep", () => {
describe("Prowler Cloud trial/onboarding (manual scan only)", () => {
beforeEach(() => {
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
scanOnDemandMock.mockResolvedValue({ data: { id: "scan-1" } });
});
@@ -11,8 +11,7 @@ import {
saveScheduleWithInitialScan,
} from "@/components/scans/schedule/save-schedule";
import { ScanScheduleFields } from "@/components/scans/schedule/scan-schedule-fields";
import { Field, FieldLabel } from "@/components/shadcn";
import { ToastAction, useToast } from "@/components/shadcn";
import { Field, FieldLabel, ToastAction, useToast } from "@/components/shadcn";
import { Badge } from "@/components/shadcn/badge/badge";
import { EntityInfo } from "@/components/shadcn/entities";
import {
@@ -11,6 +11,7 @@ import {
TestConnectionForm,
TestConnectionProviderData,
} from "../../workflow/forms/test-connection-form";
import {
WIZARD_FOOTER_ACTION_TYPE,
WizardFooterConfig,
@@ -37,6 +37,7 @@ import {
} from "@/types";
import { ProviderTitleDocs } from "../provider-title-docs";
import {
AlibabaCloudRoleCredentialsForm,
AlibabaCloudStaticCredentialsForm,
@@ -11,8 +11,7 @@ import { addProvider } from "@/actions/providers/providers";
import { AwsMethodSelector } from "@/components/providers/organizations/aws-method-selector";
import { WizardInputField } from "@/components/providers/workflow/forms/fields";
import { ProviderTitleDocs } from "@/components/providers/workflow/provider-title-docs";
import { Button } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { Button, useToast } from "@/components/shadcn";
import { Form } from "@/components/shadcn/form";
import {
addProviderFormSchema,
@@ -14,6 +14,7 @@ import {
} from "@/components/shadcn/select/select";
import { Separator } from "@/components/shadcn/separator/separator";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
import { isCloud } from "@/lib/shared/env";
import { AWSCredentialsRole } from "@/types";
import { IntegrationType } from "@/types/integrations";
@@ -36,7 +37,7 @@ export const AWSRoleCredentialsForm = ({
type?: "providers" | "integrations";
integrationType?: IntegrationType;
}) => {
const isCloudEnv = process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true";
const isCloudEnv = isCloud();
const defaultCredentialsType = isCloudEnv
? "aws-sdk-default"
: "access-secret-key";
@@ -14,8 +14,7 @@ import { CheckIcon } from "@/components/icons";
import { Button } from "@/components/shadcn";
import { Form } from "@/components/shadcn/form";
import { testProviderConnection } from "@/lib/provider-helpers";
import { ProviderType } from "@/types";
import { testConnectionFormSchema } from "@/types";
import { ProviderType, testConnectionFormSchema } from "@/types";
import { ProviderConnectionInfo } from "./provider-connection-info";
@@ -1,7 +1,6 @@
"use client";
import { Control } from "react-hook-form";
import { useWatch } from "react-hook-form";
import { Control, useWatch } from "react-hook-form";
import { WizardTextareaField } from "@/components/providers/workflow/forms/fields";
import { KubernetesCredentials } from "@/types";
@@ -48,16 +48,6 @@ export const OracleCloudCredentialsForm = ({
variant="bordered"
isRequired
/>
<WizardInputField
control={control}
name={ProviderCredentialFields.OCI_REGION}
type="text"
label="Region"
labelPlacement="inside"
placeholder="e.g. us-ashburn-1"
variant="bordered"
isRequired
/>
<WizardTextareaField
control={control}
name={ProviderCredentialFields.OCI_KEY_CONTENT}
@@ -1,7 +1,9 @@
"use client";
import { getProviderName } from "@/components/shadcn/entities/get-provider-logo";
import { getProviderLogo } from "@/components/shadcn/entities/get-provider-logo";
import {
getProviderName,
getProviderLogo,
} from "@/components/shadcn/entities/get-provider-logo";
import { ProviderType } from "@/types";
export const ProviderTitleDocs = ({
@@ -18,8 +18,6 @@ import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/shadcn";
import {
BreadcrumbNavigation,
CustomBreadcrumbItem,
} from "@/components/shadcn";
@@ -96,7 +96,7 @@ describe("AddRoleForm", () => {
it("shows Manage Alerts in Prowler Cloud", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
// When
render(<AddRoleForm groups={[]} />);
@@ -108,7 +108,7 @@ describe("AddRoleForm", () => {
it("hides Manage Alerts outside Prowler Cloud", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
// When
render(<AddRoleForm groups={[]} />);
@@ -6,6 +6,7 @@ import { DefaultValues } from "react-hook-form";
import { addRole } from "@/actions/roles/roles";
import { useToast } from "@/components/shadcn";
import { getErrorMessage } from "@/lib";
import { isCloud } from "@/lib/shared/env";
import { RoleFormValues } from "@/types";
import { RoleForm, RoleFormSubmitContext, RoleGroupOption } from "./role-form";
@@ -13,7 +14,7 @@ import { RoleForm, RoleFormSubmitContext, RoleGroupOption } from "./role-form";
export const AddRoleForm = ({ groups }: { groups: RoleGroupOption[] }) => {
const { toast } = useToast();
const router = useRouter();
const isCloudEnvironment = process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true";
const isCloudEnvironment = isCloud();
const defaultValues: DefaultValues<RoleFormValues> = {
name: "",
@@ -7,8 +7,7 @@ import * as z from "zod";
import { deleteRole } from "@/actions/roles";
import { DeleteIcon } from "@/components/icons";
import { Button } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { Button, useToast } from "@/components/shadcn";
import { Form } from "@/components/shadcn/form";
const formSchema = z.object({
@@ -6,6 +6,7 @@ import { DefaultValues } from "react-hook-form";
import { updateRole } from "@/actions/roles/roles";
import { useToast } from "@/components/shadcn";
import { getErrorMessage } from "@/lib";
import { isCloud } from "@/lib/shared/env";
import { RoleFormValues } from "@/types";
import { RoleForm, RoleFormSubmitContext, RoleGroupOption } from "./role-form";
@@ -30,7 +31,7 @@ export const EditRoleForm = ({
}) => {
const { toast } = useToast();
const router = useRouter();
const isCloudEnvironment = process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true";
const isCloudEnvironment = isCloud();
const defaultValues: DefaultValues<RoleFormValues> = {
...roleData.data.attributes,
@@ -35,6 +35,7 @@ import {
getUnlimitedVisibilityField,
getVisiblePermissionFormFields,
} from "@/lib/role-permissions";
import { isCloud } from "@/lib/shared/env";
import { roleFormSchema, RoleFormValues } from "@/types";
import { UnlimitedVisibilityField } from "./unlimited-visibility-section";
@@ -74,7 +75,7 @@ export const RoleForm = ({
defaultValues,
});
const isCloudEnvironment = process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true";
const isCloudEnvironment = isCloud();
const visiblePermissionFormFields =
getVisiblePermissionFormFields(isCloudEnvironment);
const showUnlimitedVisibilityField = !!getUnlimitedVisibilityField();
@@ -2,19 +2,18 @@
import { useControlledState } from "@react-stately/utils";
import { domAnimation, LazyMotion, m } from "framer-motion";
import type { ComponentProps } from "react";
import React from "react";
import { forwardRef, useMemo } from "react";
import type { ComponentProps, HTMLAttributes, ReactNode } from "react";
import { cn } from "@/lib/utils";
export type VerticalStepProps = {
className?: string;
description?: React.ReactNode;
title?: React.ReactNode;
description?: ReactNode;
title?: ReactNode;
};
export interface VerticalStepsProps
extends React.HTMLAttributes<HTMLButtonElement> {
export interface VerticalStepsProps extends HTMLAttributes<HTMLButtonElement> {
/**
* An array of steps.
*
@@ -89,10 +88,7 @@ function CheckIcon(props: ComponentProps<"svg">) {
);
}
export const VerticalSteps = React.forwardRef<
HTMLButtonElement,
VerticalStepsProps
>(
export const VerticalSteps = forwardRef<HTMLButtonElement, VerticalStepsProps>(
(
{
color = "primary",
@@ -113,7 +109,7 @@ export const VerticalSteps = React.forwardRef<
onStepChange,
);
const colors = React.useMemo(() => {
const colors = useMemo(() => {
let userColor;
let fgColor;
+15 -15
View File
@@ -154,7 +154,7 @@ describe("ScansPageShell", () => {
});
it("does not render an imported findings tab", () => {
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
render(
<ScansPageShell providers={providers} hasManageScansPermission>
@@ -172,7 +172,7 @@ describe("ScansPageShell", () => {
});
it("uses the shared scan filter bar for scan filters", () => {
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
render(
<ScansPageShell providers={providers} hasManageScansPermission>
@@ -190,7 +190,7 @@ describe("ScansPageShell", () => {
});
it("clears the active sort when switching tabs", async () => {
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
searchParamsValue.current = "tab=active&sort=trigger";
const user = userEvent.setup();
@@ -209,7 +209,7 @@ describe("ScansPageShell", () => {
});
it("uses a generic type filter label in Cloud", () => {
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
render(
<ScansPageShell providers={providers} hasManageScansPermission>
@@ -221,7 +221,7 @@ describe("ScansPageShell", () => {
});
it("shows the CLI import banner in Cloud", () => {
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
render(
<ScansPageShell providers={providers} hasManageScansPermission>
@@ -239,7 +239,7 @@ describe("ScansPageShell", () => {
});
it("hides the CLI import banner outside Cloud", () => {
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
render(
<ScansPageShell providers={providers} hasManageScansPermission>
@@ -251,7 +251,7 @@ describe("ScansPageShell", () => {
});
it("keeps launch scan with filters and mutelist with tabs", () => {
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
render(
<ScansPageShell providers={providers} hasManageScansPermission>
@@ -273,7 +273,7 @@ describe("ScansPageShell", () => {
});
it("shows the active scans count in the in progress tab", () => {
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
render(
<ScansPageShell
@@ -292,7 +292,7 @@ describe("ScansPageShell", () => {
});
it("opens the launch scan modal from the URL", () => {
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
searchParamsValue.current = "launchScan=true";
render(
@@ -305,7 +305,7 @@ describe("ScansPageShell", () => {
});
it("strips the launchScan URL param via the History API when closing the URL-opened modal", async () => {
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
searchParamsValue.current = "tab=completed&launchScan=true";
const replaceStateSpy = vi.spyOn(window.history, "replaceState");
const user = userEvent.setup();
@@ -333,7 +333,7 @@ describe("ScansPageShell", () => {
});
it("opens and closes the launch scan modal from client state without navigation", async () => {
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
const user = userEvent.setup();
useScansStore.getState().openLaunchScanModal();
@@ -352,7 +352,7 @@ describe("ScansPageShell", () => {
});
it("shows the status filter only on the completed tab", () => {
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
searchParamsValue.current = "tab=completed";
render(
@@ -367,7 +367,7 @@ describe("ScansPageShell", () => {
});
it("hides the status filter outside of the completed tab", () => {
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
searchParamsValue.current = "tab=active";
render(
@@ -382,7 +382,7 @@ describe("ScansPageShell", () => {
});
it("clears status filter when switching scan tabs", async () => {
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
searchParamsValue.current = "tab=completed&filter%5Bstate__in%5D=failed";
const user = userEvent.setup();
@@ -400,7 +400,7 @@ describe("ScansPageShell", () => {
});
it("clears type filter when switching to scheduled scans", async () => {
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
searchParamsValue.current = "tab=completed&filter%5Btrigger%5D=manual";
const user = userEvent.setup();
@@ -3,7 +3,11 @@ import { render, screen } from "@testing-library/react";
import type { ReactNode } from "react";
import { describe, expect, it, vi } from "vitest";
import type { ScanProps } from "@/types";
import { type ScanProps, SCAN_JOBS_TAB, type ScanJobsTab } from "@/types";
import {
SCAN_SCHEDULE_CAPABILITY,
type ScanScheduleCapability,
} from "@/types/schedules";
vi.mock("@/components/shadcn", async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
@@ -65,12 +69,6 @@ vi.mock("./scan-jobs-row-actions", () => ({
),
}));
import { SCAN_JOBS_TAB, type ScanJobsTab } from "@/types";
import {
SCAN_SCHEDULE_CAPABILITY,
type ScanScheduleCapability,
} from "@/types/schedules";
import { getScanJobsColumns } from "./scan-jobs-columns";
const getColumnIds = (tab: ScanJobsTab) =>
@@ -10,6 +10,7 @@ import { SCAN_JOBS_TAB, type ScanJobsTab, type ScanProps } from "@/types";
import type { ScanScheduleCapability } from "@/types/schedules";
import { formatScanDuration } from "../scans.utils";
import {
AccountCell,
ProgressCell,
@@ -151,7 +151,7 @@ describe("ScanJobsRowActions", () => {
it("opens Edit Scan Schedule for Prowler Cloud subscribed scan rows", async () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
const user = userEvent.setup();
render(<ScanJobsRowActions scan={makeScan()} tab="scheduled" />);
@@ -173,7 +173,7 @@ describe("ScanJobsRowActions", () => {
it("hides Edit Scan Schedule outside Prowler Cloud (OSS)", async () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
const user = userEvent.setup();
render(<ScanJobsRowActions scan={makeScan()} tab="scheduled" />);
@@ -191,7 +191,7 @@ describe("ScanJobsRowActions", () => {
it("hides Edit Scan Schedule outside the Scheduled tab even on Cloud", async () => {
// Given - advanced capability (Cloud) but rendered in the Completed tab.
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
const user = userEvent.setup();
render(
@@ -214,7 +214,7 @@ describe("ScanJobsRowActions", () => {
it("hides Edit Scan Schedule for manual-only Cloud scan rows", async () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
const user = userEvent.setup();
render(
@@ -238,7 +238,7 @@ describe("ScanJobsRowActions", () => {
it("hides Edit Scan Schedule for blocked Cloud scan rows", async () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
const user = userEvent.setup();
render(
@@ -7,6 +7,7 @@ import type { ScanScheduleCapability } from "@/types/schedules";
import { AutoRefresh } from "../auto-refresh";
import { NoScansEmptyState } from "../no-scans-empty-state";
import { getScanJobsColumns } from "./scan-jobs-columns";
interface ScanJobsTableProps {
@@ -6,6 +6,7 @@ import { ComponentProps, ReactNode, useEffect, useState } from "react";
import { cn } from "@/lib/utils";
import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip";
import {
DropdownMenu,
DropdownMenuContent,
@@ -7,16 +7,47 @@ import { CLOUD_UPGRADE_FEATURE } from "@/types/cloud-upgrade";
import { CloudUpgradeModal } from "./cloud-upgrade-modal";
const modalTestState = vi.hoisted(() => ({
keepContentMounted: false,
}));
vi.mock("@/components/shadcn/modal", async (importOriginal) => {
const actual =
await importOriginal<typeof import("@/components/shadcn/modal")>();
const { createElement } = await import("react");
return {
...actual,
Modal: (props: Parameters<typeof actual.Modal>[0]) => {
if (!modalTestState.keepContentMounted) {
return createElement(actual.Modal, props);
}
return createElement(
"div",
{ "aria-label": props.title, role: "dialog" },
createElement(
"button",
{ onClick: () => props.onOpenChange?.(false), type: "button" },
"Close",
),
props.children,
);
},
};
});
describe("CloudUpgradeModal", () => {
afterEach(() => {
cleanup();
modalTestState.keepContentMounted = false;
vi.unstubAllEnvs();
useCloudUpgradeStore.getState().closeCloudUpgrade();
});
it("renders the active contextual upgrade in Local Server", async () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
useCloudUpgradeStore
.getState()
.openCloudUpgrade(CLOUD_UPGRADE_FEATURE.ALERTS);
@@ -45,7 +76,7 @@ describe("CloudUpgradeModal", () => {
it("uses the standard equal-width CTA layout", async () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
useCloudUpgradeStore
.getState()
.openCloudUpgrade(CLOUD_UPGRADE_FEATURE.AWS_ORGANIZATIONS);
@@ -119,7 +150,7 @@ describe("CloudUpgradeModal", () => {
it("closes the active upgrade and returns focus to its trigger", async () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
const user = userEvent.setup();
render(
@@ -152,9 +183,45 @@ describe("CloudUpgradeModal", () => {
expect(useCloudUpgradeStore.getState().activeFeature).toBeNull();
});
it.each([
{
feature: CLOUD_UPGRADE_FEATURE.ALERTS,
otherTitle: "Add Your Entire AWS Organization",
title: "Turn Findings into Alerts",
},
{
feature: CLOUD_UPGRADE_FEATURE.AWS_ORGANIZATIONS,
otherTitle: "Turn Findings into Alerts",
title: "Add Your Entire AWS Organization",
},
])(
"does not replace $title with another upgrade while closing",
async ({ feature, otherTitle, title }) => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
modalTestState.keepContentMounted = true;
const user = userEvent.setup();
useCloudUpgradeStore.getState().openCloudUpgrade(feature);
render(<CloudUpgradeModal />);
expect(screen.getByRole("dialog", { name: title })).toBeVisible();
// When
await user.click(screen.getByRole("button", { name: "Close" }));
// Then
expect(useCloudUpgradeStore.getState().activeFeature).toBeNull();
expect(screen.getByRole("dialog", { name: title })).toBeVisible();
expect(
screen.queryByText("Scale Prowler Without Operating It"),
).not.toBeInTheDocument();
expect(screen.queryByText(otherTitle)).not.toBeInTheDocument();
},
);
it("does not render upgrade UI in Prowler Cloud", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
useCloudUpgradeStore
.getState()
.openCloudUpgrade(CLOUD_UPGRADE_FEATURE.ALERTS);
+4 -2
View File
@@ -14,12 +14,14 @@ import {
} 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 retainedFeature = useCloudUpgradeStore(
(state) => state.retainedFeature,
);
const closeCloudUpgrade = useCloudUpgradeStore(
(state) => state.closeCloudUpgrade,
);
@@ -29,7 +31,7 @@ export const CloudUpgradeModal = () => {
if (isCloud()) return null;
const feature = activeFeature ?? CLOUD_UPGRADE_FEATURE.GENERAL;
const feature = activeFeature ?? retainedFeature;
const content = CLOUD_UPGRADE_CONTENT[feature];
return (
+1 -2
View File
@@ -7,8 +7,7 @@ import * as z from "zod";
import { deleteUser } from "@/actions/users/users";
import { DeleteIcon } from "@/components/icons";
import { Button } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { Button, useToast } from "@/components/shadcn";
import { Form } from "@/components/shadcn/form";
const formSchema = z.object({
+1 -2
View File
@@ -7,8 +7,7 @@ import { Controller, useForm } from "react-hook-form";
import * as z from "zod";
import { updateUser, updateUserRole } from "@/actions/users/users";
import { Card } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { Card, useToast } from "@/components/shadcn";
import { CustomInput } from "@/components/shadcn/custom";
import { Form, FormButtons } from "@/components/shadcn/form";
import {
@@ -4,8 +4,7 @@ import { Dispatch, SetStateAction, useTransition } from "react";
import { removeUserFromTenant } from "@/actions/users/users";
import { DeleteIcon } from "@/components/icons";
import { Button } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { Button, useToast } from "@/components/shadcn";
interface ExpelUserFormProps {
userId: string;
@@ -34,7 +34,7 @@ describe("RoleItem", () => {
it("shows Manage Alerts in Prowler Cloud role details", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
// When
render(<RoleItem role={role} roleDetail={roleDetail} />);
@@ -45,7 +45,7 @@ describe("RoleItem", () => {
it("hides Manage Alerts outside Prowler Cloud role details", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
// When
render(<RoleItem role={role} roleDetail={roleDetail} />);
@@ -56,7 +56,7 @@ describe("RoleItem", () => {
it("displays the permission state as a badge", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
// When
render(<RoleItem role={role} roleDetail={roleDetail} />);
@@ -67,7 +67,7 @@ describe("RoleItem", () => {
it("does not render the details toggle", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
// When
render(<RoleItem role={role} roleDetail={roleDetail} />);