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:
Pablo F.G
2026-07-16 14:26:50 +02:00
parent 59b13778e2
commit 375a8a5c55
7 changed files with 120 additions and 70 deletions
+11 -4
View File
@@ -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", () => {
+24 -52
View File
@@ -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();
});
});
+11
View File
@@ -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 />
);
}