mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 21:11:53 +00:00
fix(ui): always show Scans page so imported scans are visible
- Render the scans table regardless of provider connection status - Show no-provider / not-connected onboarding help as a non-blocking hint - Soften "connection required" copy; drop the forced onboarding redirect
This commit is contained in:
@@ -9,11 +9,18 @@ describe("scans page onboarding", () => {
|
||||
const pagePath = path.join(currentDir, "page.tsx");
|
||||
const source = readFileSync(pagePath, "utf8");
|
||||
|
||||
it("redirects the scan tour replay to add-provider when providers are missing or disconnected", () => {
|
||||
expect(source).toContain('redirect("/providers?onboarding=add-provider")');
|
||||
expect(source).toContain(
|
||||
'resolvedSearchParams.onboarding === "view-first-scan"',
|
||||
it("never blocks or redirects away from Scans when providers are missing or disconnected", () => {
|
||||
// Imported scans belong to a never-connected provider, so the page must render the
|
||||
// table regardless of provider connection status instead of gating/redirecting.
|
||||
expect(source).not.toContain(
|
||||
'redirect("/providers?onboarding=add-provider")',
|
||||
);
|
||||
expect(source).not.toContain("missingScanPrerequisite");
|
||||
});
|
||||
|
||||
it("always renders the scans table shell", () => {
|
||||
expect(source).toContain("<ScansPageShell");
|
||||
expect(source).toContain("<SSRDataTableScans");
|
||||
});
|
||||
|
||||
it("passes the scan onboarding action to the page header when the tour can run", () => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { getAllProviderGroups } from "@/actions/manage-groups/manage-groups";
|
||||
@@ -10,7 +9,6 @@ import {
|
||||
} from "@/actions/scans/scans-filters";
|
||||
import { getSchedules, getSchedulesPage } from "@/actions/schedules";
|
||||
import { auth } from "@/auth.config";
|
||||
import { PageReady } from "@/components/onboarding";
|
||||
import {
|
||||
appendPendingScheduleRowsToPage,
|
||||
buildScheduledTabRows,
|
||||
@@ -21,7 +19,6 @@ import {
|
||||
pickScheduleProviderFilters,
|
||||
} from "@/components/scans/scans.utils";
|
||||
import { ScansPageShell } from "@/components/scans/scans-page-shell";
|
||||
import { ScansProvidersEmptyState } from "@/components/scans/scans-providers-empty-state";
|
||||
import { SkeletonTableScans } from "@/components/scans/table";
|
||||
import { ScanJobsTable } from "@/components/scans/table/scan-jobs-table";
|
||||
import { ContentLayout } from "@/components/shadcn/content-layout";
|
||||
@@ -188,36 +185,22 @@ export default async function Scans({
|
||||
const providers = providersData?.data ?? [];
|
||||
const providerGroups = providerGroupsData?.data ?? [];
|
||||
|
||||
const connectedProviders = providers.filter(
|
||||
const hasConnectedProvider = providers.some(
|
||||
(provider: ProviderProps) =>
|
||||
provider.attributes.connection.connected === true,
|
||||
);
|
||||
const thereIsNoProviders = providers.length === 0;
|
||||
const thereIsNoProvidersConnected =
|
||||
!thereIsNoProviders && connectedProviders.length === 0;
|
||||
const missingScanPrerequisite =
|
||||
thereIsNoProviders || thereIsNoProvidersConnected;
|
||||
|
||||
if (
|
||||
missingScanPrerequisite &&
|
||||
resolvedSearchParams.onboarding === "view-first-scan"
|
||||
) {
|
||||
redirect("/providers?onboarding=add-provider");
|
||||
}
|
||||
|
||||
const hasManageScansPermission = Boolean(
|
||||
session?.user?.permissions?.manage_scans,
|
||||
);
|
||||
const activeScanCount = missingScanPrerequisite
|
||||
? 0
|
||||
: await getActiveScanCount(resolvedSearchParams);
|
||||
const onboardingAction = missingScanPrerequisite
|
||||
? {
|
||||
const activeScanCount = await getActiveScanCount(resolvedSearchParams);
|
||||
const onboardingAction = hasConnectedProvider
|
||||
? { flowId: "view-first-scan" }
|
||||
: {
|
||||
flowId: "view-first-scan",
|
||||
fallbackFlowId: "add-provider",
|
||||
useFallback: true,
|
||||
}
|
||||
: { flowId: "view-first-scan" };
|
||||
};
|
||||
|
||||
return (
|
||||
<ContentLayout
|
||||
@@ -225,36 +208,25 @@ export default async function Scans({
|
||||
icon="lucide:timer"
|
||||
onboardingAction={onboardingAction}
|
||||
>
|
||||
{missingScanPrerequisite ? (
|
||||
<>
|
||||
{/* The populated branch mounts <PageReady/> inside ScansPageShell to
|
||||
enable the navbar tour icon. The empty branch must mark the route
|
||||
ready too, otherwise the icon (which falls back to the add-provider
|
||||
flow here) stays hidden for users with no connected provider. */}
|
||||
<PageReady />
|
||||
<ScansProvidersEmptyState thereIsNoProviders={thereIsNoProviders} />
|
||||
</>
|
||||
) : (
|
||||
<ScansPageShell
|
||||
providers={providers}
|
||||
providerGroups={providerGroups}
|
||||
hasManageScansPermission={hasManageScansPermission}
|
||||
activeScanCount={activeScanCount}
|
||||
>
|
||||
<Suspense
|
||||
fallback={
|
||||
<SkeletonTableScans
|
||||
tab={getScanJobsTab(resolvedSearchParams.tab)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<SSRDataTableScans
|
||||
searchParams={resolvedSearchParams}
|
||||
providers={providers}
|
||||
<ScansPageShell
|
||||
providers={providers}
|
||||
providerGroups={providerGroups}
|
||||
hasManageScansPermission={hasManageScansPermission}
|
||||
activeScanCount={activeScanCount}
|
||||
>
|
||||
<Suspense
|
||||
fallback={
|
||||
<SkeletonTableScans
|
||||
tab={getScanJobsTab(resolvedSearchParams.tab)}
|
||||
/>
|
||||
</Suspense>
|
||||
</ScansPageShell>
|
||||
)}
|
||||
}
|
||||
>
|
||||
<SSRDataTableScans
|
||||
searchParams={resolvedSearchParams}
|
||||
providers={providers}
|
||||
/>
|
||||
</Suspense>
|
||||
</ScansPageShell>
|
||||
</ContentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Imported scans now appear on the Scans page even when no provider is connected
|
||||
@@ -18,12 +18,8 @@ export const NoProvidersConnected = () => {
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
No providers are currently connected. Connecting a provider is
|
||||
required to launch on-demand scans.
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
Once the providers are correctly configured, this message will
|
||||
disappear, and on-demand scans can be launched.
|
||||
None of your providers are connected yet. Connect one to launch
|
||||
on-demand scans — imported scans still appear below.
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-full md:w-auto md:shrink-0">
|
||||
|
||||
@@ -144,6 +144,19 @@ const providers: ProviderProps[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const disconnectedProviders: ProviderProps[] = [
|
||||
{
|
||||
...providers[0],
|
||||
attributes: {
|
||||
...providers[0].attributes,
|
||||
connection: {
|
||||
...providers[0].attributes.connection,
|
||||
connected: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe("ScansPageShell", () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
@@ -416,4 +429,51 @@ describe("ScansPageShell", () => {
|
||||
expect(calledUrl).toContain("tab=scheduled");
|
||||
expect(calledUrl).not.toContain("filter%5Btrigger%5D");
|
||||
});
|
||||
|
||||
it("shows a non-blocking hint when no provider is connected, while still rendering the table", () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
|
||||
render(
|
||||
<ScansPageShell
|
||||
providers={disconnectedProviders}
|
||||
hasManageScansPermission
|
||||
>
|
||||
<div>Scans table</div>
|
||||
</ScansPageShell>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("No Connected Providers")).toBeInTheDocument();
|
||||
// The table (and therefore imported scans) must still render below the hint.
|
||||
expect(screen.getByText("Scans table")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows the no-providers hint when there are no providers, while still rendering the table", () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
|
||||
render(
|
||||
<ScansPageShell providers={[]} hasManageScansPermission>
|
||||
<div>Scans table</div>
|
||||
</ScansPageShell>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("No Providers Configured")).toBeInTheDocument();
|
||||
expect(screen.getByText("Scans table")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not show the providers hint when a provider is connected", () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
|
||||
render(
|
||||
<ScansPageShell providers={providers} hasManageScansPermission>
|
||||
<div>Scans table</div>
|
||||
</ScansPageShell>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByText("No Connected Providers"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText("No Providers Configured"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,6 +30,7 @@ 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 { ScansProvidersEmptyState } from "./scans-providers-empty-state";
|
||||
import { useScansFilters } from "./use-scans-filters";
|
||||
|
||||
interface ScansPageShellProps {
|
||||
@@ -68,6 +69,10 @@ export function ScansPageShell({
|
||||
const hasConnectedProviders = providers.some(
|
||||
(provider) => provider.attributes.connection.connected === true,
|
||||
);
|
||||
const thereAreNoProviders = providers.length === 0;
|
||||
// Non-blocking onboarding hint: shown when scans can't be launched (no provider, or
|
||||
// none connected). The table still renders below, so imported scans stay visible.
|
||||
const showProvidersHint = thereAreNoProviders || !hasConnectedProviders;
|
||||
const isCloudEnvironment = isCloud();
|
||||
const launchDisabled = !hasManageScansPermission || !hasConnectedProviders;
|
||||
const launchOpen = isLaunchScanModalOpen || urlLaunchOpen;
|
||||
@@ -113,6 +118,12 @@ export function ScansPageShell({
|
||||
</Suspense>
|
||||
{/* Signals the navbar that this route's data has loaded (enables the replay icon). */}
|
||||
<PageReady />
|
||||
{showProvidersHint && (
|
||||
<ScansProvidersEmptyState
|
||||
thereIsNoProviders={thereAreNoProviders}
|
||||
containerClassName="min-h-0"
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
role="group"
|
||||
aria-label="Scan filters and actions"
|
||||
|
||||
@@ -5,18 +5,21 @@ import { NoProvidersConnected } from "./no-providers-connected";
|
||||
|
||||
interface ScansProvidersEmptyStateProps {
|
||||
thereIsNoProviders: boolean;
|
||||
/** Overrides the NoProvidersAdded container height so it can sit as a compact top hint. */
|
||||
containerClassName?: string;
|
||||
}
|
||||
|
||||
export function ScansProvidersEmptyState({
|
||||
thereIsNoProviders,
|
||||
containerClassName,
|
||||
}: ScansProvidersEmptyStateProps) {
|
||||
return (
|
||||
<>
|
||||
{thereIsNoProviders ? (
|
||||
<NoProvidersAdded action="link" href={ADD_PROVIDER_HREF} />
|
||||
) : (
|
||||
<NoProvidersConnected />
|
||||
)}
|
||||
</>
|
||||
return thereIsNoProviders ? (
|
||||
<NoProvidersAdded
|
||||
action="link"
|
||||
href={ADD_PROVIDER_HREF}
|
||||
containerClassName={containerClassName}
|
||||
/>
|
||||
) : (
|
||||
<NoProvidersConnected />
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user