mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-04 02:04:06 +00:00
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
145 lines
5.4 KiB
TypeScript
145 lines
5.4 KiB
TypeScript
import "@/styles/globals.css";
|
|
|
|
import * as Sentry from "@sentry/nextjs";
|
|
import { Metadata, Viewport } from "next";
|
|
import { ReactNode, Suspense } from "react";
|
|
|
|
import { getProviders } from "@/actions/providers";
|
|
import { getScansByState } from "@/actions/scans/scans";
|
|
import { auth } from "@/auth.config";
|
|
import MainLayout from "@/components/layout/main-layout/main-layout";
|
|
import {
|
|
OnboardingCheckpointWatcher,
|
|
OnboardingGate,
|
|
OnboardingSequenceBanner,
|
|
} from "@/components/onboarding";
|
|
import { RuntimePublicConfig } from "@/components/runtime-config/runtime-public-config";
|
|
import { NavigationProgress } from "@/components/shadcn/navigation-progress";
|
|
import { Toaster } from "@/components/shadcn/toast";
|
|
import { TaskPollingWatcher } from "@/components/shared/task-polling-watcher";
|
|
import { GlobalSidePanel } from "@/components/side-panel";
|
|
import { FeedbackSurvey } from "@/components/survey/feedback-survey";
|
|
import { fontMono, fontSans } from "@/config/fonts";
|
|
import { siteConfig } from "@/config/site";
|
|
import { REGISTRY_ACCESS } from "@/lib/registry/access";
|
|
import { evaluateRegistryAccess } from "@/lib/registry/access.server";
|
|
import { isCloud } from "@/lib/shared/env";
|
|
import { cn } from "@/lib/utils";
|
|
import { StoreInitializer } from "@/store/ui/store-initializer";
|
|
import { SCAN_STATES } from "@/types/attack-paths";
|
|
|
|
import { Providers } from "../providers";
|
|
|
|
export const metadata: Metadata = {
|
|
title: {
|
|
default: siteConfig.name,
|
|
template: `%s - ${siteConfig.name}`,
|
|
},
|
|
description: siteConfig.description,
|
|
icons: {
|
|
icon: "/favicon.ico",
|
|
},
|
|
other: {
|
|
...Sentry.getTraceData(),
|
|
},
|
|
};
|
|
|
|
export const viewport: Viewport = {
|
|
themeColor: [
|
|
{ media: "(prefers-color-scheme: light)", color: "white" },
|
|
{ media: "(prefers-color-scheme: dark)", color: "black" },
|
|
],
|
|
};
|
|
|
|
export default async function RootLayout({
|
|
children,
|
|
}: {
|
|
children: ReactNode;
|
|
}) {
|
|
// Skip Cloud-only onboarding fetches and orchestrators in OSS.
|
|
const cloudEnabled = isCloud();
|
|
|
|
// Every deployment needs the provider count: it drives the first-run redirect
|
|
// and the sidebar's Add Provider action.
|
|
const providersPromise = getProviders({ page: 1, pageSize: 1 });
|
|
|
|
// One-time server-side Registry gate per request: only an ELIGIBLE answer
|
|
// shows the sidebar entry; UNKNOWN and INELIGIBLE both hide it. Started
|
|
// here so it resolves in parallel with the Cloud onboarding fetches.
|
|
const registryAccessPromise = auth().then((session) =>
|
|
evaluateRegistryAccess(session?.accessToken),
|
|
);
|
|
|
|
// Fail-open: unknown scan state is treated as "has data" so the banner never blocks
|
|
// progression on a fetch error.
|
|
let hasCompletedScan = true;
|
|
// Scopes the onboarding steps' local markers, so resolving them for one
|
|
// tenant does not silence them for another.
|
|
let tenantId: string | null = null;
|
|
|
|
if (cloudEnabled) {
|
|
const scansByState = await getScansByState();
|
|
hasCompletedScan = Array.isArray(scansByState?.data)
|
|
? scansByState.data.some(
|
|
(scan: { attributes?: { state?: string } }) =>
|
|
scan.attributes?.state === SCAN_STATES.COMPLETED,
|
|
)
|
|
: true;
|
|
tenantId = (await auth())?.tenantId ?? null;
|
|
}
|
|
|
|
const providersData = await providersPromise;
|
|
// Tri-state: true = has providers, false = zero providers, undefined = fetch failed (gate fails open).
|
|
const hasProviders: boolean | undefined = Array.isArray(providersData?.data)
|
|
? providersData.data.length > 0
|
|
: undefined;
|
|
|
|
const registryEligible =
|
|
(await registryAccessPromise).status === REGISTRY_ACCESS.ELIGIBLE;
|
|
|
|
return (
|
|
<html suppressHydrationWarning lang="en">
|
|
<head>
|
|
<RuntimePublicConfig />
|
|
</head>
|
|
<body
|
|
suppressHydrationWarning
|
|
className={cn(
|
|
"bg-bg-neutral-primary min-h-screen font-sans antialiased",
|
|
fontSans.variable,
|
|
fontMono.variable,
|
|
)}
|
|
>
|
|
<Providers themeProps={{ attribute: "class", defaultTheme: "dark" }}>
|
|
{/* Suspense contains the useSearchParams() CSR bailout so statically
|
|
prerendered pages don't fail the build (matches the auth layout). */}
|
|
<Suspense>
|
|
<NavigationProgress />
|
|
</Suspense>
|
|
{/* Tri-state for both: an unknown count leaves the store unresolved and the gate closed. */}
|
|
<StoreInitializer values={{ hasProviders, registryEligible }} />
|
|
{/* Every deployment: an empty tenant lands on the add-provider wizard once. */}
|
|
<OnboardingGate hasProviders={hasProviders} tenantId={tenantId} />
|
|
{cloudEnabled && (
|
|
<>
|
|
{/* Single mount point so the watcher survives post-connect navigation. */}
|
|
<OnboardingCheckpointWatcher tenantId={tenantId} />
|
|
{/* Persistent banner shown only while a guided sequence is active. */}
|
|
<OnboardingSequenceBanner hasCompletedScan={hasCompletedScan} />
|
|
</>
|
|
)}
|
|
<MainLayout>{children}</MainLayout>
|
|
{cloudEnabled && <FeedbackSurvey />}
|
|
{/* Always mounted: it hosts the detail (finding/resource) views in
|
|
every deployment; the AI tab inside is cloud-gated on its own. */}
|
|
<GlobalSidePanel />
|
|
{/* Resumes persisted background-task polling (e.g. cross-provider
|
|
PDF generation) so completion toasts survive hard reloads. */}
|
|
<TaskPollingWatcher />
|
|
<Toaster />
|
|
</Providers>
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|