feat(ui): reused custom banner component

This commit is contained in:
Pablo F.G
2026-07-17 11:57:07 +02:00
parent d38489e4c0
commit 03b6e8c60b
4 changed files with 31 additions and 119 deletions
+6 -53
View File
@@ -11,21 +11,10 @@ const NO_PROVIDERS_ADDED_ACTION = {
LINK: "link",
} as const;
// "page" is the full-screen empty state (Providers page); "hint" is a compact
// horizontal banner that sits above other content (Scans page).
const NO_PROVIDERS_ADDED_VARIANT = {
PAGE: "page",
HINT: "hint",
} as const;
type NoProvidersAddedVariant =
(typeof NO_PROVIDERS_ADDED_VARIANT)[keyof typeof NO_PROVIDERS_ADDED_VARIANT];
interface NoProvidersAddedBaseProps {
containerClassName?: string;
// Tour anchor for the CTA; needed because this empty state replaces the table's AddProviderButton.
ctaTourId?: string;
variant?: NoProvidersAddedVariant;
}
interface NoProvidersAddedButtonProps extends NoProvidersAddedBaseProps {
@@ -44,23 +33,14 @@ type NoProvidersAddedProps =
| NoProvidersAddedButtonProps
| NoProvidersAddedLinkProps;
const renderCta = (
props: NoProvidersAddedProps,
variant: NoProvidersAddedVariant,
) => {
const isHint = variant === NO_PROVIDERS_ADDED_VARIANT.HINT;
const className = isHint
? "w-full justify-center md:w-fit"
: "w-full max-w-xs justify-center";
const size = isHint ? undefined : "lg";
const renderCta = (props: NoProvidersAddedProps) => {
if (props.action === NO_PROVIDERS_ADDED_ACTION.LINK) {
return (
<Button
asChild
aria-label="Open Add Provider modal"
className={className}
size={size}
className="w-full max-w-xs justify-center"
size="lg"
>
<Link href={props.href}>Add a Provider</Link>
</Button>
@@ -70,9 +50,9 @@ const renderCta = (
return (
<Button
aria-label="Open Add Provider modal"
className={className}
className="w-full max-w-xs justify-center"
data-tour-id={props.ctaTourId}
size={size}
size="lg"
onClick={props.onOpenWizard}
>
Add a Provider
@@ -81,33 +61,6 @@ const renderCta = (
};
export const NoProvidersAdded = (props: NoProvidersAddedProps) => {
const variant = props.variant ?? NO_PROVIDERS_ADDED_VARIANT.PAGE;
// Compact horizontal hint, matching NoProvidersConnected so both provider
// hints on the Scans page share one composition.
if (variant === NO_PROVIDERS_ADDED_VARIANT.HINT) {
return (
<Card variant="base">
<CardContent className="flex w-full flex-col items-start gap-6 md:flex-row md:items-center md:justify-between md:gap-8">
<div className="flex flex-col gap-3">
<div className="flex items-center justify-start gap-3">
<InfoIcon className="h-6 w-6 text-gray-800 dark:text-white" />
<h2 className="text-lg font-bold text-gray-800 dark:text-white">
No Providers Configured
</h2>
</div>
<p className="text-sm text-gray-600 dark:text-gray-300">
No providers have been configured. Start by setting up a provider.
</p>
</div>
<div className="w-full md:w-auto md:shrink-0">
{renderCta(props, variant)}
</div>
</CardContent>
</Card>
);
}
return (
<div
role="region"
@@ -134,7 +87,7 @@ export const NoProvidersAdded = (props: NoProvidersAddedProps) => {
</p>
</div>
{renderCta(props, variant)}
{renderCta(props)}
</CardContent>
</Card>
</div>
@@ -1,37 +0,0 @@
"use client";
import Link from "next/link";
import { Button, Card, CardContent } from "@/components/shadcn";
import { InfoIcon } from "../icons/Icons";
export const NoProvidersConnected = () => {
return (
<Card variant="base">
<CardContent className="flex w-full flex-col items-start gap-6 md:flex-row md:items-center md:justify-between md:gap-8">
<div className="flex flex-col gap-3">
<div className="flex items-center justify-start gap-3">
<InfoIcon className="h-6 w-6 text-gray-800 dark:text-white" />
<h2 className="text-lg font-bold text-gray-800 dark:text-white">
No Connected Providers
</h2>
</div>
<p className="text-sm text-gray-600 dark:text-gray-300">
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">
<Button
asChild
className="w-full justify-center md:w-fit"
aria-label="Go to Providers page"
>
<Link href="/providers">Review Providers</Link>
</Button>
</div>
</CardContent>
</Card>
);
};
@@ -1,44 +1,32 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { describe, expect, it } from "vitest";
import { ADD_PROVIDER_HREF } from "@/lib/providers-navigation";
import { ScansProvidersEmptyState } from "./scans-providers-empty-state";
vi.mock("./no-providers-connected", () => ({
NoProvidersConnected: () => <div>No Connected Providers</div>,
}));
describe("ScansProvidersEmptyState", () => {
it("shows the add provider message with a providers page CTA", () => {
// Given/When
it("shows the add-provider hint with a providers page CTA when there are no providers", () => {
render(<ScansProvidersEmptyState thereIsNoProviders />);
// Then
expect(screen.getByText("No Providers Configured")).toBeInTheDocument();
const cta = screen.getByRole("link", {
name: /open add provider modal/i,
});
const cta = screen.getByRole("link", { name: /add a provider/i });
expect(cta).toHaveAttribute("href", ADD_PROVIDER_HREF);
expect(cta.tagName).toBe("A");
});
it("does not render the provider wizard in Scans", () => {
// Given/When
render(<ScansProvidersEmptyState thereIsNoProviders />);
// Then
expect(screen.getByText("No Providers Configured")).toBeInTheDocument();
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
it("shows the no connected providers message", () => {
// Given/When
it("shows the no-connected-providers hint with a providers page CTA", () => {
render(<ScansProvidersEmptyState thereIsNoProviders={false} />);
// Then
expect(screen.getByText("No Connected Providers")).toBeInTheDocument();
const cta = screen.getByRole("link", { name: /review providers/i });
expect(cta).toHaveAttribute("href", "/providers");
expect(cta.tagName).toBe("A");
});
it("does not render the provider wizard dialog in Scans", () => {
render(<ScansProvidersEmptyState thereIsNoProviders />);
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
});
@@ -1,8 +1,6 @@
import { NoProvidersAdded } from "@/components/providers/no-providers-added";
import { CustomBanner } from "@/components/shadcn/custom/custom-banner";
import { ADD_PROVIDER_HREF } from "@/lib/providers-navigation";
import { NoProvidersConnected } from "./no-providers-connected";
interface ScansProvidersEmptyStateProps {
thereIsNoProviders: boolean;
}
@@ -11,8 +9,18 @@ export function ScansProvidersEmptyState({
thereIsNoProviders,
}: ScansProvidersEmptyStateProps) {
return thereIsNoProviders ? (
<NoProvidersAdded action="link" href={ADD_PROVIDER_HREF} variant="hint" />
<CustomBanner
title="No Providers Configured"
message="No providers have been configured. Start by setting up a provider."
buttonLabel="Add a Provider"
buttonLink={ADD_PROVIDER_HREF}
/>
) : (
<NoProvidersConnected />
<CustomBanner
title="No Connected Providers"
message="None of your providers are connected yet. Connect one to launch on-demand scans — imported scans still appear below."
buttonLabel="Review Providers"
buttonLink="/providers"
/>
);
}