mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
feat(ui): move Lighthouse banner to top of cloud overview
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user