feat: add TableLoadingRow and TableEmptyStateRow components for improved table state handling

This commit is contained in:
Abhimanyu Saharan
2026-02-09 00:07:01 +05:30
parent 1340e00b61
commit 746b909ed6
5 changed files with 159 additions and 182 deletions

View File

@@ -28,6 +28,7 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { TableEmptyStateRow, TableLoadingRow } from "@/components/ui/table-state";
import { ApiError } from "@/api/mutator"; import { ApiError } from "@/api/mutator";
import { import {
@@ -291,11 +292,7 @@ export default function AgentsPage() {
</thead> </thead>
<tbody className="divide-y divide-slate-100"> <tbody className="divide-y divide-slate-100">
{agentsQuery.isLoading ? ( {agentsQuery.isLoading ? (
<tr> <TableLoadingRow colSpan={columns.length} />
<td colSpan={columns.length} className="px-6 py-8">
<span className="text-sm text-slate-500">Loading</span>
</td>
</tr>
) : table.getRowModel().rows.length ? ( ) : table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row) => ( table.getRowModel().rows.map((row) => (
<tr key={row.id} className="hover:bg-slate-50"> <tr key={row.id} className="hover:bg-slate-50">
@@ -310,10 +307,9 @@ export default function AgentsPage() {
</tr> </tr>
)) ))
) : ( ) : (
<tr> <TableEmptyStateRow
<td colSpan={columns.length} className="px-6 py-16"> colSpan={columns.length}
<div className="flex flex-col items-center justify-center text-center"> icon={
<div className="mb-4 rounded-full bg-slate-50 p-4">
<svg <svg
className="h-16 w-16 text-slate-300" className="h-16 w-16 text-slate-300"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -328,26 +324,12 @@ export default function AgentsPage() {
<path d="M22 21v-2a4 4 0 0 0-3-3.87" /> <path d="M22 21v-2a4 4 0 0 0-3-3.87" />
<path d="M16 3.13a4 4 0 0 1 0 7.75" /> <path d="M16 3.13a4 4 0 0 1 0 7.75" />
</svg> </svg>
</div> }
<h3 className="mb-2 text-lg font-semibold text-slate-900"> title="No agents yet"
No agents yet description="Create your first agent to start executing tasks on this board."
</h3> actionHref="/agents/new"
<p className="mb-6 max-w-md text-sm text-slate-500"> actionLabel="Create your first agent"
Create your first agent to start executing tasks on />
this board.
</p>
<Link
href="/agents/new"
className={buttonVariants({
size: "md",
variant: "primary",
})}
>
Create your first agent
</Link>
</div>
</td>
</tr>
)} )}
</tbody> </tbody>
</table> </table>

View File

@@ -35,6 +35,7 @@ import {
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { SignedOutPanel } from "@/components/auth/SignedOutPanel"; import { SignedOutPanel } from "@/components/auth/SignedOutPanel";
import { formatTimestamp } from "@/lib/formatters"; import { formatTimestamp } from "@/lib/formatters";
import { TableEmptyStateRow, TableLoadingRow } from "@/components/ui/table-state";
export default function BoardGroupsPage() { export default function BoardGroupsPage() {
const { isSignedIn } = useAuth(); const { isSignedIn } = useAuth();
@@ -235,13 +236,7 @@ export default function BoardGroupsPage() {
</thead> </thead>
<tbody className="divide-y divide-slate-100"> <tbody className="divide-y divide-slate-100">
{groupsQuery.isLoading ? ( {groupsQuery.isLoading ? (
<tr> <TableLoadingRow colSpan={columns.length} />
<td colSpan={columns.length} className="px-6 py-8">
<span className="text-sm text-slate-500">
Loading
</span>
</td>
</tr>
) : table.getRowModel().rows.length ? ( ) : table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row) => ( table.getRowModel().rows.map((row) => (
<tr <tr
@@ -259,10 +254,9 @@ export default function BoardGroupsPage() {
</tr> </tr>
)) ))
) : ( ) : (
<tr> <TableEmptyStateRow
<td colSpan={columns.length} className="px-6 py-16"> colSpan={columns.length}
<div className="flex flex-col items-center justify-center text-center"> icon={
<div className="mb-4 rounded-full bg-slate-50 p-4">
<svg <svg
className="h-16 w-16 text-slate-300" className="h-16 w-16 text-slate-300"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -278,26 +272,12 @@ export default function BoardGroupsPage() {
<path d="M13 17h8" /> <path d="M13 17h8" />
<path d="M3 12h18" /> <path d="M3 12h18" />
</svg> </svg>
</div> }
<h3 className="mb-2 text-lg font-semibold text-slate-900"> title="No groups yet"
No groups yet description="Create a board group to increase cross-board visibility for agents."
</h3> actionHref="/board-groups/new"
<p className="mb-6 max-w-md text-sm text-slate-500"> actionLabel="Create your first group"
Create a board group to increase cross-board />
visibility for agents.
</p>
<Link
href="/board-groups/new"
className={buttonVariants({
size: "md",
variant: "primary",
})}
>
Create your first group
</Link>
</div>
</td>
</tr>
)} )}
</tbody> </tbody>
</table> </table>

View File

@@ -40,6 +40,7 @@ import {
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { SignedOutPanel } from "@/components/auth/SignedOutPanel"; import { SignedOutPanel } from "@/components/auth/SignedOutPanel";
import { TableEmptyStateRow, TableLoadingRow } from "@/components/ui/table-state";
const compactId = (value: string) => const compactId = (value: string) =>
value.length > 8 ? `${value.slice(0, 8)}` : value; value.length > 8 ? `${value.slice(0, 8)}` : value;
@@ -290,13 +291,7 @@ export default function BoardsPage() {
</thead> </thead>
<tbody className="divide-y divide-slate-100"> <tbody className="divide-y divide-slate-100">
{boardsQuery.isLoading ? ( {boardsQuery.isLoading ? (
<tr> <TableLoadingRow colSpan={columns.length} />
<td colSpan={columns.length} className="px-6 py-8">
<span className="text-sm text-slate-500">
Loading
</span>
</td>
</tr>
) : table.getRowModel().rows.length ? ( ) : table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row) => ( table.getRowModel().rows.map((row) => (
<tr <tr
@@ -314,10 +309,9 @@ export default function BoardsPage() {
</tr> </tr>
)) ))
) : ( ) : (
<tr> <TableEmptyStateRow
<td colSpan={columns.length} className="px-6 py-16"> colSpan={columns.length}
<div className="flex flex-col items-center justify-center text-center"> icon={
<div className="mb-4 rounded-full bg-slate-50 p-4">
<svg <svg
className="h-16 w-16 text-slate-300" className="h-16 w-16 text-slate-300"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -332,26 +326,12 @@ export default function BoardsPage() {
<rect x="14" y="14" width="7" height="7" /> <rect x="14" y="14" width="7" height="7" />
<rect x="3" y="14" width="7" height="7" /> <rect x="3" y="14" width="7" height="7" />
</svg> </svg>
</div> }
<h3 className="mb-2 text-lg font-semibold text-slate-900"> title="No boards yet"
No boards yet description="Create your first board to start routing tasks and monitoring work across agents."
</h3> actionHref="/boards/new"
<p className="mb-6 max-w-md text-sm text-slate-500"> actionLabel="Create your first board"
Create your first board to start routing tasks and />
monitoring work across agents.
</p>
<Link
href="/boards/new"
className={buttonVariants({
size: "md",
variant: "primary",
})}
>
Create your first board
</Link>
</div>
</td>
</tr>
)} )}
</tbody> </tbody>
</table> </table>

View File

@@ -26,6 +26,7 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { TableEmptyStateRow, TableLoadingRow } from "@/components/ui/table-state";
import { ApiError } from "@/api/mutator"; import { ApiError } from "@/api/mutator";
import { import {
@@ -246,11 +247,7 @@ export default function GatewaysPage() {
</thead> </thead>
<tbody className="divide-y divide-slate-100"> <tbody className="divide-y divide-slate-100">
{gatewaysQuery.isLoading ? ( {gatewaysQuery.isLoading ? (
<tr> <TableLoadingRow colSpan={columns.length} />
<td colSpan={columns.length} className="px-6 py-8">
<span className="text-sm text-slate-500">Loading</span>
</td>
</tr>
) : table.getRowModel().rows.length ? ( ) : table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row) => ( table.getRowModel().rows.map((row) => (
<tr key={row.id} className="hover:bg-slate-50"> <tr key={row.id} className="hover:bg-slate-50">
@@ -265,10 +262,9 @@ export default function GatewaysPage() {
</tr> </tr>
)) ))
) : ( ) : (
<tr> <TableEmptyStateRow
<td colSpan={columns.length} className="px-6 py-16"> colSpan={columns.length}
<div className="flex flex-col items-center justify-center text-center"> icon={
<div className="mb-4 rounded-full bg-slate-50 p-4">
<svg <svg
className="h-16 w-16 text-slate-300" className="h-16 w-16 text-slate-300"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -278,36 +274,15 @@ export default function GatewaysPage() {
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
> >
<rect <rect x="2" y="7" width="20" height="14" rx="2" ry="2" />
x="2"
y="7"
width="20"
height="14"
rx="2"
ry="2"
/>
<path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16" /> <path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16" />
</svg> </svg>
</div> }
<h3 className="mb-2 text-lg font-semibold text-slate-900"> title="No gateways yet"
No gateways yet description="Create your first gateway to connect boards and start managing your OpenClaw connections."
</h3> actionHref="/gateways/new"
<p className="mb-6 max-w-md text-sm text-slate-500"> actionLabel="Create your first gateway"
Create your first gateway to connect boards and start />
managing your OpenClaw connections.
</p>
<Link
href="/gateways/new"
className={buttonVariants({
size: "md",
variant: "primary",
})}
>
Create your first gateway
</Link>
</div>
</td>
</tr>
)} )}
</tbody> </tbody>
</table> </table>

View File

@@ -0,0 +1,60 @@
import type { ReactNode } from "react";
import Link from "next/link";
import { buttonVariants } from "@/components/ui/button";
type TableLoadingRowProps = {
colSpan: number;
label?: string;
};
export function TableLoadingRow({
colSpan,
label = "Loading…",
}: TableLoadingRowProps) {
return (
<tr>
<td colSpan={colSpan} className="px-6 py-8">
<span className="text-sm text-slate-500">{label}</span>
</td>
</tr>
);
}
type TableEmptyStateRowProps = {
colSpan: number;
icon: ReactNode;
title: string;
description: string;
actionHref?: string;
actionLabel?: string;
};
export function TableEmptyStateRow({
colSpan,
icon,
title,
description,
actionHref,
actionLabel,
}: TableEmptyStateRowProps) {
return (
<tr>
<td colSpan={colSpan} className="px-6 py-16">
<div className="flex flex-col items-center justify-center text-center">
<div className="mb-4 rounded-full bg-slate-50 p-4">{icon}</div>
<h3 className="mb-2 text-lg font-semibold text-slate-900">{title}</h3>
<p className="mb-6 max-w-md text-sm text-slate-500">{description}</p>
{actionHref && actionLabel ? (
<Link
href={actionHref}
className={buttonVariants({ size: "md", variant: "primary" })}
>
{actionLabel}
</Link>
) : null}
</div>
</td>
</tr>
);
}