Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions app/src/app/(dashboard)/connections/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1156,13 +1156,23 @@ export default function ConnectionsPage() {
<EmptyState
icon={<Database className="h-12 w-12" />}
title="No connections yet"
description="Add your first database connection to start querying data."
description="Connect a database to start building dashboards."
action={
<Button onClick={() => openCreateDialog()}>
<Plus className="mr-2 h-4 w-4" />
Add your first connection
Create your first connection
</Button>
}
secondaryAction={
<a
href="https://neoboard.app/docs/getting-started/quick-start/"
target="_blank"
rel="noopener noreferrer"
className="text-primary underline-offset-4 hover:underline"
>
Read the docs
</a>
}
/>
)}
</LoadingOverlay>
Expand Down
22 changes: 10 additions & 12 deletions app/src/app/(dashboard)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -599,18 +599,16 @@ export default function DashboardListPage() {
<EmptyState
icon={<LayoutDashboard className="h-12 w-12" />}
title="No dashboards yet"
description="No dashboards have been shared with you yet. Ask an admin or editor to share one, or read the docs to learn what NeoBoard can do."
action={
<Button variant="outline" asChild>
<a
href="https://neoboard.app/docs/getting-started/quick-start/"
target="_blank"
rel="noopener noreferrer"
>
<BookOpen className="mr-2 h-4 w-4" />
Read the docs
</a>
</Button>
description="Ask an admin or editor to share one with you."
secondaryAction={
<a
href="https://neoboard.app/docs/getting-started/quick-start/"
target="_blank"
rel="noopener noreferrer"
className="text-primary underline-offset-4 hover:underline"
>
Read the docs
</a>
}
/>
)
Expand Down
16 changes: 13 additions & 3 deletions app/src/app/(dashboard)/users/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -582,14 +582,24 @@ export default function UsersPage() {
) : !users?.length ? (
<EmptyState
icon={<UsersIcon className="h-12 w-12" />}
title="No users found"
description="Create your first user to get started."
title="No users yet"
description="Add team members so they can collaborate on dashboards."
action={
<Button onClick={() => setShowCreate(true)}>
<Plus className="mr-2 h-4 w-4" />
Create User
Create your first user
</Button>
}
secondaryAction={
<a
href="https://neoboard.app/docs/getting-started/quick-start/"
target="_blank"
rel="noopener noreferrer"
className="text-primary underline-offset-4 hover:underline"
>
Read the docs
</a>
}
/>
) : (
<DataGrid
Expand Down
18 changes: 17 additions & 1 deletion app/src/app/(dashboard)/widget-lab/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,23 @@ export default function WidgetLabPage() {
<EmptyState
icon={<FlaskConical className="h-12 w-12" />}
title="No templates yet"
description='Create a new template or save a widget from any dashboard using the "Save to Widget Lab" action.'
description="Reusable widgets you can drop into any dashboard."
action={
<Button onClick={handleCreate}>
<Plus className="mr-2 h-4 w-4" />
Create your first template
</Button>
}
secondaryAction={
<a
href="https://neoboard.app/docs/getting-started/quick-start/"
target="_blank"
rel="noopener noreferrer"
className="text-primary underline-offset-4 hover:underline"
>
Read the docs
</a>
}
/>
) : (
<EmptyState
Expand Down
61 changes: 55 additions & 6 deletions component/src/components/composed/__tests__/empty-state.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,76 @@ describe("EmptyState", () => {
});

it("renders description when provided", () => {
render(<EmptyState title="No results" description="Try adjusting your search" />);
render(
<EmptyState title="No results" description="Try adjusting your search" />,
);
expect(screen.getByText("Try adjusting your search")).toBeInTheDocument();
});

it("does not render description when not provided", () => {
render(<EmptyState title="No results" />);
expect(screen.queryByText("Try adjusting your search")).not.toBeInTheDocument();
expect(
screen.queryByText("Try adjusting your search"),
).not.toBeInTheDocument();
});

it("renders icon when provided", () => {
render(<EmptyState title="No results" icon={<span data-testid="icon">!</span>} />);
render(
<EmptyState
title="No results"
icon={<span data-testid="icon">!</span>}
/>,
);
expect(screen.getByTestId("icon")).toBeInTheDocument();
});

it("renders action when provided", () => {
render(<EmptyState title="No results" action={<button>Create new</button>} />);
expect(screen.getByRole("button", { name: "Create new" })).toBeInTheDocument();
render(
<EmptyState title="No results" action={<button>Create new</button>} />,
);
expect(
screen.getByRole("button", { name: "Create new" }),
).toBeInTheDocument();
});

it("applies custom className", () => {
const { container } = render(<EmptyState title="No results" className="my-class" />);
const { container } = render(
<EmptyState title="No results" className="my-class" />,
);
expect(container.firstChild).toHaveClass("my-class");
});

it("renders secondaryAction when provided", () => {
render(
<EmptyState
title="No results"
action={<button>Create new</button>}
secondaryAction={<a href="/docs">Read the docs</a>}
/>,
);
expect(
screen.getByRole("link", { name: "Read the docs" }),
).toBeInTheDocument();
});

it("does not render secondaryAction container when not provided", () => {
render(
<EmptyState title="No results" action={<button>Create new</button>} />,
);
expect(
screen.queryByRole("link", { name: "Read the docs" }),
).not.toBeInTheDocument();
});

it("renders secondaryAction even when primary action is absent", () => {
render(
<EmptyState
title="No results"
secondaryAction={<a href="/docs">Read the docs</a>}
/>,
);
expect(
screen.getByRole("link", { name: "Read the docs" }),
).toBeInTheDocument();
});
});
14 changes: 10 additions & 4 deletions component/src/components/composed/empty-state.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export interface EmptyStateProps {
title: string;
description?: string;
action?: React.ReactNode;
/** Optional secondary action (e.g. a "Read the docs" link) rendered below `action`. */
secondaryAction?: React.ReactNode;
className?: string;
}

Expand All @@ -14,25 +16,29 @@ function EmptyState({
title,
description,
action,
secondaryAction,
className,
}: EmptyStateProps) {
return (
<div
className={cn(
"flex flex-col items-center justify-center py-12 px-4 text-center",
className
className,
)}
>
{icon && (
<div className="mb-4 text-muted-foreground">{icon}</div>
)}
{icon && <div className="mb-4 text-muted-foreground">{icon}</div>}
<h3 className="text-lg font-semibold">{title}</h3>
{description && (
<p className="mt-2 text-sm text-muted-foreground max-w-sm">
{description}
</p>
)}
{action && <div className="mt-6">{action}</div>}
{secondaryAction && (
<div className={cn(action ? "mt-3" : "mt-6", "text-sm")}>
{secondaryAction}
</div>
)}
</div>
);
}
Expand Down
Loading