feat(ui): move Lighthouse banner to top of cloud overview

This commit is contained in:
alejandrobailo
2026-07-02 15:22:44 +02:00
parent aed4dd5990
commit c8b7d2a0f2
6 changed files with 249 additions and 3 deletions
@@ -0,0 +1,33 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { LighthouseOverviewBanner } from "./lighthouse-overview-banner";
describe("LighthouseOverviewBanner", () => {
it("renders Toni copy and links to Lighthouse when connected", () => {
// Given / When
render(<LighthouseOverviewBanner href="/lighthouse" />);
// Then
const link = screen.getByRole("link", {
name: /Find and remediate which actually matters\./,
});
expect(link).toHaveAttribute("href", "/lighthouse");
expect(link).toHaveTextContent("Lighthouse AI");
expect(link).toHaveTextContent(
"Find and remediate which actually matters.",
);
});
it("links to Lighthouse settings when no connected configuration exists", () => {
// Given / When
render(<LighthouseOverviewBanner href="/lighthouse/settings" />);
// Then
expect(
screen.getByRole("link", {
name: /Find and remediate which actually matters\./,
}),
).toHaveAttribute("href", "/lighthouse/settings");
});
});
@@ -0,0 +1,45 @@
import { ArrowRight } from "lucide-react";
import Link from "next/link";
import { LighthouseIcon } from "@/components/icons/Icons";
import { Card, CardContent } from "@/components/shadcn";
import type { LighthouseOverviewBannerHref } from "../_lib/lighthouse-banner";
interface LighthouseOverviewBannerProps {
href: LighthouseOverviewBannerHref;
}
export function LighthouseOverviewBanner({
href,
}: LighthouseOverviewBannerProps) {
return (
<Link
href={href}
className="group focus-visible:ring-border-input-primary block rounded-xl focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
>
<Card
variant="base"
padding="none"
className="group-hover:border-border-input-primary transition-colors"
>
<CardContent className="flex min-w-0 items-center justify-between gap-4 px-4 py-3 sm:px-5">
<div className="flex min-w-0 items-center gap-3">
<span className="border-border-neutral-tertiary bg-bg-neutral-tertiary flex size-9 shrink-0 items-center justify-center rounded-md border">
<LighthouseIcon className="size-5" />
</span>
<div className="min-w-0">
<p className="text-text-neutral-primary text-sm font-medium">
Lighthouse AI
</p>
<p className="text-text-neutral-secondary text-sm">
Find and remediate which actually matters.
</p>
</div>
</div>
<ArrowRight className="text-text-neutral-tertiary size-4 shrink-0 transition-transform group-hover:translate-x-0.5" />
</CardContent>
</Card>
</Link>
);
}
@@ -0,0 +1,105 @@
import { describe, expect, it, vi } from "vitest";
import type {
LighthouseV2Configuration,
LighthouseV2ProviderType,
} from "@/app/(prowler)/lighthouse/_types";
import {
getLighthouseOverviewBannerHref,
resolveLighthouseOverviewBannerHref,
} from "./lighthouse-banner";
describe("resolveLighthouseOverviewBannerHref", () => {
it("routes to Lighthouse chat when any v2 configuration is connected", () => {
// Given / When
const href = resolveLighthouseOverviewBannerHref([
configuration("openai", false),
configuration("bedrock", true),
]);
// Then
expect(href).toBe("/lighthouse");
});
it("routes to Lighthouse settings when no v2 configuration is connected", () => {
// Given / When
const href = resolveLighthouseOverviewBannerHref([
configuration("openai", false),
configuration("openai-compatible", null),
]);
// Then
expect(href).toBe("/lighthouse/settings");
});
});
describe("getLighthouseOverviewBannerHref", () => {
it("hides the banner outside cloud without loading configurations", async () => {
// Given
const loadConfigurations = vi.fn(async () => ({
data: [configuration("openai", true)],
}));
// When
const href = await getLighthouseOverviewBannerHref(
false,
loadConfigurations,
);
// Then
expect(href).toBeNull();
expect(loadConfigurations).not.toHaveBeenCalled();
});
it("hides the banner when configurations fail to load", async () => {
// Given
const loadConfigurations = vi.fn(async () => ({
error: "Unauthorized",
status: 401,
}));
// When
const href = await getLighthouseOverviewBannerHref(
true,
loadConfigurations,
);
// Then
expect(href).toBeNull();
});
it("resolves the banner href from loaded configurations in cloud", async () => {
// Given
const loadConfigurations = vi.fn(async () => ({
data: [configuration("bedrock", true)],
}));
// When
const href = await getLighthouseOverviewBannerHref(
true,
loadConfigurations,
);
// Then
expect(href).toBe("/lighthouse");
});
});
function configuration(
providerType: LighthouseV2ProviderType,
connected: LighthouseV2Configuration["connected"],
): LighthouseV2Configuration {
return {
id: `config-${providerType}`,
providerType,
baseUrl:
providerType === "openai-compatible" ? "https://example.com" : null,
defaultModel: null,
businessContext: "Production account",
connected,
connectionLastCheckedAt: null,
insertedAt: "2026-06-24T09:00:00Z",
updatedAt: "2026-06-24T10:00:00Z",
};
}
@@ -0,0 +1,52 @@
import type { LighthouseV2Configuration } from "@/app/(prowler)/lighthouse/_types";
export const LIGHTHOUSE_OVERVIEW_BANNER_HREF = {
CHAT: "/lighthouse",
SETTINGS: "/lighthouse/settings",
} as const;
export type LighthouseOverviewBannerHref =
(typeof LIGHTHOUSE_OVERVIEW_BANNER_HREF)[keyof typeof LIGHTHOUSE_OVERVIEW_BANNER_HREF];
interface LighthouseV2ConfigurationsSuccess {
data: LighthouseV2Configuration[];
}
interface LighthouseV2ConfigurationsFailure {
error: string;
errors?: unknown[];
status?: number;
}
type LighthouseV2ConfigurationsResult =
| LighthouseV2ConfigurationsSuccess
| LighthouseV2ConfigurationsFailure;
type LoadLighthouseV2Configurations =
() => Promise<LighthouseV2ConfigurationsResult>;
export function resolveLighthouseOverviewBannerHref(
configurations: LighthouseV2Configuration[],
): LighthouseOverviewBannerHref {
return configurations.some(
(configuration) => configuration.connected === true,
)
? LIGHTHOUSE_OVERVIEW_BANNER_HREF.CHAT
: LIGHTHOUSE_OVERVIEW_BANNER_HREF.SETTINGS;
}
export async function getLighthouseOverviewBannerHref(
cloud: boolean,
loadConfigurations: LoadLighthouseV2Configurations,
): Promise<LighthouseOverviewBannerHref | null> {
if (!cloud) {
return null;
}
const result = await loadConfigurations();
if (!("data" in result)) {
return null;
}
return resolveLighthouseOverviewBannerHref(result.data);
}
@@ -1,7 +1,6 @@
"use server";
import { getLatestFindings } from "@/actions/findings/findings";
import { LighthouseBanner } from "@/components/lighthouse-v1/banner";
import { LinkToFindings } from "@/components/overview";
import { ColumnLatestFindings } from "@/components/overview/new-findings-table/table";
import { CardTitle } from "@/components/shadcn";
@@ -60,7 +59,6 @@ export async function FindingsViewSSR({ searchParams }: FindingsViewSSRProps) {
return (
<div className="flex w-full flex-col">
<LighthouseBanner />
<DataTable
key={`dashboard-findings-${Date.now()}`}
columns={ColumnLatestFindings}
+14 -1
View File
@@ -1,10 +1,14 @@
import { Suspense } from "react";
import { getAllProviders } from "@/actions/providers";
import { getLighthouseV2Configurations } from "@/app/(prowler)/lighthouse/_actions";
import { ProviderAccountSelectors } from "@/components/filters/provider-account-selectors";
import { ContentLayout } from "@/components/ui";
import { isCloud } from "@/lib/shared/env";
import { SearchParamsProps } from "@/types";
import { LighthouseOverviewBanner } from "./_overview/_components/lighthouse-overview-banner";
import { getLighthouseOverviewBannerHref } from "./_overview/_lib/lighthouse-banner";
import {
AttackSurfaceSkeleton,
AttackSurfaceSSR,
@@ -38,7 +42,10 @@ export default async function Home({
searchParams: Promise<SearchParamsProps>;
}) {
const resolvedSearchParams = await searchParams;
const providersData = await getAllProviders();
const [providersData, lighthouseBannerHref] = await Promise.all([
getAllProviders(),
getLighthouseOverviewBannerHref(isCloud(), getLighthouseV2Configurations),
]);
return (
<ContentLayout title="Overview" icon="lucide:square-chart-gantt">
@@ -46,6 +53,12 @@ export default async function Home({
<ProviderAccountSelectors providers={providersData?.data ?? []} />
</div>
{lighthouseBannerHref ? (
<div className="mb-6">
<LighthouseOverviewBanner href={lighthouseBannerHref} />
</div>
) : null}
<div className="flex flex-col gap-6 xl:flex-row xl:flex-wrap xl:items-stretch">
<Suspense fallback={<ThreatScoreSkeleton />}>
<ThreatScoreSSR searchParams={resolvedSearchParams} />