Files
prowler/ui/components/scans/scans-page-shell.tsx
T
Pablo Fernandez Guerra (PFE) 5b9824c379 feat(ui): filter by provider group across main views (#11659)
Co-authored-by: Pablo F.G <pablo.fernandez@prowler.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 15:32:00 +02:00

184 lines
5.9 KiB
TypeScript

"use client";
import { usePathname, useSearchParams } from "next/navigation";
import { type ReactNode, Suspense, useState } from "react";
import { OnboardingTrigger, PageReady } from "@/components/onboarding";
import { MutedFindingsConfigButton } from "@/components/providers/muted-findings-config-button";
import {
Button,
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/shadcn";
import { getFlowById } from "@/lib/onboarding";
import {
LAUNCH_SCAN_SEARCH_PARAM,
LAUNCH_SCAN_SEARCH_VALUE,
} from "@/lib/scans-navigation";
import { buildViewFirstScanTour } from "@/lib/tours/view-first-scan.tour";
import { useScansStore } from "@/store";
import { SCAN_JOBS_TAB, SCAN_TAB_LABELS, type ScanJobsTab } from "@/types";
import type { ProviderGroup } from "@/types/components";
import type { ProviderProps } from "@/types/providers";
import type { ScanScheduleCapability } from "@/types/schedules";
const viewFirstScanFlow = getFlowById("view-first-scan")!;
import { CliImportBanner } from "./cli-import-banner";
import { LaunchScanModal } from "./launch-scan-modal";
import { ScansFilterBar } from "./scans-filter-bar";
import { useScansFilters } from "./use-scans-filters";
interface ScansPageShellProps {
providers: ProviderProps[];
providerGroups?: ProviderGroup[];
hasManageScansPermission: boolean;
activeScanCount?: number;
children: ReactNode;
/** Cloud overlay seam for the launch-scan modal. */
scanScheduleCapability?: ScanScheduleCapability;
isScanLimitReached?: boolean;
}
export function ScansPageShell({
providers,
providerGroups = [],
hasManageScansPermission,
activeScanCount = 0,
children,
scanScheduleCapability,
isScanLimitReached,
}: ScansPageShellProps) {
const pathname = usePathname();
const searchParams = useSearchParams();
const [urlLaunchOpen, setUrlLaunchOpen] = useState(
() =>
searchParams.get(LAUNCH_SCAN_SEARCH_PARAM) === LAUNCH_SCAN_SEARCH_VALUE,
);
const isLaunchScanModalOpen = useScansStore(
(state) => state.isLaunchScanModalOpen,
);
const setLaunchScanModalOpen = useScansStore(
(state) => state.setLaunchScanModalOpen,
);
const filters = useScansFilters();
const hasConnectedProviders = providers.some(
(provider) => provider.attributes.connection.connected === true,
);
const isCloudEnvironment = process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true";
const launchDisabled = !hasManageScansPermission || !hasConnectedProviders;
const launchOpen = isLaunchScanModalOpen || urlLaunchOpen;
// When a scan is already running, the tour highlights its row (anchored in
// ScanJobsTable); otherwise it falls back to the Launch Scan button + tabs.
const hasInProgressScan = activeScanCount > 0;
const getTabLabel = (tab: ScanJobsTab) => {
const label = SCAN_TAB_LABELS[tab];
if (tab !== SCAN_JOBS_TAB.ACTIVE) return label;
return `${label} (${activeScanCount})`;
};
const handleLaunchOpenChange = (open: boolean) => {
setLaunchScanModalOpen(open);
if (open) return;
setUrlLaunchOpen(false);
// Remove ?launchScan via History API (not router.replace) to avoid an RSC
// refetch that reloads the page; revalidatePath in scanOnDemand already
// refreshes the scans list when a scan is launched.
if (!searchParams.has(LAUNCH_SCAN_SEARCH_PARAM)) return;
const params = new URLSearchParams(searchParams.toString());
params.delete(LAUNCH_SCAN_SEARCH_PARAM);
const query = params.toString();
window.history.replaceState(
null,
"",
query ? `${pathname}?${query}` : pathname,
);
};
return (
<div className="flex flex-col gap-[18px]">
{/* Suspense required: OnboardingTrigger reads useSearchParams */}
<Suspense fallback={null}>
<OnboardingTrigger
flow={{
...viewFirstScanFlow,
tour: buildViewFirstScanTour(hasInProgressScan),
}}
/>
</Suspense>
{/* Signals the navbar that this route's data has loaded (enables the replay icon). */}
<PageReady />
<div
role="group"
aria-label="Scan filters and actions"
className="flex flex-wrap items-center gap-3"
>
<ScansFilterBar
providers={providers}
providerGroups={providerGroups}
activeTab={filters.activeTab}
scheduleType={filters.scheduleType}
scanStatus={filters.scanStatus}
showStatusFilter={filters.showStatusFilter}
onScheduleTypeChange={filters.setScheduleType}
onScanStatusChange={filters.setScanStatus}
/>
<Button
type="button"
size="lg"
onClick={() => handleLaunchOpenChange(true)}
disabled={launchDisabled}
className="w-full md:w-auto"
data-tour-id="view-first-scan-launch"
>
Launch Scan
</Button>
</div>
{isCloudEnvironment && <CliImportBanner />}
<Tabs
value={filters.activeTab}
onValueChange={filters.setTab}
className="flex flex-col gap-[18px]"
>
<div
role="group"
aria-label="Scan tabs"
className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"
>
<TabsList
className="overflow-x-auto"
data-tour-id="view-first-scan-tabs"
>
{Object.values(SCAN_JOBS_TAB).map((tab) => (
<TabsTrigger key={tab} value={tab}>
{getTabLabel(tab as ScanJobsTab)}
</TabsTrigger>
))}
</TabsList>
<div className="shrink-0">
<MutedFindingsConfigButton />
</div>
</div>
<TabsContent value={filters.activeTab} className="mt-0">
{children}
</TabsContent>
</Tabs>
<LaunchScanModal
open={launchOpen}
onOpenChange={handleLaunchOpenChange}
providers={providers}
capability={scanScheduleCapability}
isScanLimitReached={isScanLimitReached}
/>
</div>
);
}