diff --git a/ui/app/(prowler)/_overview/_components/lighthouse-overview-banner.test.tsx b/ui/app/(prowler)/_overview/_components/lighthouse-overview-banner.test.tsx new file mode 100644 index 0000000000..2e94daf4c7 --- /dev/null +++ b/ui/app/(prowler)/_overview/_components/lighthouse-overview-banner.test.tsx @@ -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(); + + // 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(); + + // Then + expect( + screen.getByRole("link", { + name: /Find and remediate which actually matters\./, + }), + ).toHaveAttribute("href", "/lighthouse/settings"); + }); +}); diff --git a/ui/app/(prowler)/_overview/_components/lighthouse-overview-banner.tsx b/ui/app/(prowler)/_overview/_components/lighthouse-overview-banner.tsx new file mode 100644 index 0000000000..e76e6e83a2 --- /dev/null +++ b/ui/app/(prowler)/_overview/_components/lighthouse-overview-banner.tsx @@ -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 ( + + + +
+ + + +
+

+ Lighthouse AI +

+

+ Find and remediate which actually matters. +

+
+
+ +
+
+ + ); +} diff --git a/ui/app/(prowler)/_overview/_lib/lighthouse-banner.test.ts b/ui/app/(prowler)/_overview/_lib/lighthouse-banner.test.ts new file mode 100644 index 0000000000..2ac8bcb2df --- /dev/null +++ b/ui/app/(prowler)/_overview/_lib/lighthouse-banner.test.ts @@ -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", + }; +} diff --git a/ui/app/(prowler)/_overview/_lib/lighthouse-banner.ts b/ui/app/(prowler)/_overview/_lib/lighthouse-banner.ts new file mode 100644 index 0000000000..cab8757218 --- /dev/null +++ b/ui/app/(prowler)/_overview/_lib/lighthouse-banner.ts @@ -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; + +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 { + if (!cloud) { + return null; + } + + const result = await loadConfigurations(); + if (!("data" in result)) { + return null; + } + + return resolveLighthouseOverviewBannerHref(result.data); +} diff --git a/ui/app/(prowler)/_overview/graphs-tabs/findings-view/findings-view.ssr.tsx b/ui/app/(prowler)/_overview/graphs-tabs/findings-view/findings-view.ssr.tsx index df91def7d7..2f4cf64ac3 100644 --- a/ui/app/(prowler)/_overview/graphs-tabs/findings-view/findings-view.ssr.tsx +++ b/ui/app/(prowler)/_overview/graphs-tabs/findings-view/findings-view.ssr.tsx @@ -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 (
- ; }) { const resolvedSearchParams = await searchParams; - const providersData = await getAllProviders(); + const [providersData, lighthouseBannerHref] = await Promise.all([ + getAllProviders(), + getLighthouseOverviewBannerHref(isCloud(), getLighthouseV2Configurations), + ]); return ( @@ -46,6 +53,12 @@ export default async function Home({
+ {lighthouseBannerHref ? ( +
+ +
+ ) : null} +
}>