diff --git a/frontend/src/app/agents/page.tsx b/frontend/src/app/agents/page.tsx
index c0889076..c8c31952 100644
--- a/frontend/src/app/agents/page.tsx
+++ b/frontend/src/app/agents/page.tsx
@@ -28,6 +28,7 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
+import { TableEmptyStateRow, TableLoadingRow } from "@/components/ui/table-state";
import { ApiError } from "@/api/mutator";
import {
@@ -291,11 +292,7 @@ export default function AgentsPage() {
{agentsQuery.isLoading ? (
-
- |
- Loading…
- |
-
+
) : table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row) => (
@@ -310,44 +307,29 @@ export default function AgentsPage() {
))
) : (
-
-
-
-
-
- No agents yet
-
-
- Create your first agent to start executing tasks on
- this board.
-
-
- Create your first agent
-
-
- |
-
+
+
+
+
+
+
+ }
+ title="No agents yet"
+ description="Create your first agent to start executing tasks on this board."
+ actionHref="/agents/new"
+ actionLabel="Create your first agent"
+ />
)}
diff --git a/frontend/src/app/board-groups/page.tsx b/frontend/src/app/board-groups/page.tsx
index 36525a18..80e7135c 100644
--- a/frontend/src/app/board-groups/page.tsx
+++ b/frontend/src/app/board-groups/page.tsx
@@ -35,6 +35,7 @@ import {
} from "@/components/ui/dialog";
import { SignedOutPanel } from "@/components/auth/SignedOutPanel";
import { formatTimestamp } from "@/lib/formatters";
+import { TableEmptyStateRow, TableLoadingRow } from "@/components/ui/table-state";
export default function BoardGroupsPage() {
const { isSignedIn } = useAuth();
@@ -235,13 +236,7 @@ export default function BoardGroupsPage() {
{groupsQuery.isLoading ? (
-
- |
-
- Loading…
-
- |
-
+
) : table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row) => (
))
) : (
-
-
-
-
-
- No groups yet
-
-
- Create a board group to increase cross-board
- visibility for agents.
-
-
- Create your first group
-
-
- |
-
+
+
+
+
+
+
+
+ }
+ title="No groups yet"
+ description="Create a board group to increase cross-board visibility for agents."
+ actionHref="/board-groups/new"
+ actionLabel="Create your first group"
+ />
)}
diff --git a/frontend/src/app/boards/page.tsx b/frontend/src/app/boards/page.tsx
index 6c984f00..b6e67721 100644
--- a/frontend/src/app/boards/page.tsx
+++ b/frontend/src/app/boards/page.tsx
@@ -40,6 +40,7 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { SignedOutPanel } from "@/components/auth/SignedOutPanel";
+import { TableEmptyStateRow, TableLoadingRow } from "@/components/ui/table-state";
const compactId = (value: string) =>
value.length > 8 ? `${value.slice(0, 8)}…` : value;
@@ -290,13 +291,7 @@ export default function BoardsPage() {
{boardsQuery.isLoading ? (
-
- |
-
- Loading…
-
- |
-
+
) : table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row) => (
))
) : (
-
-
-
-
-
-
-
- No boards yet
-
-
- Create your first board to start routing tasks and
- monitoring work across agents.
-
-
- Create your first board
-
-
- |
-
+
+
+
+
+
+
+ }
+ title="No boards yet"
+ description="Create your first board to start routing tasks and monitoring work across agents."
+ actionHref="/boards/new"
+ actionLabel="Create your first board"
+ />
)}
diff --git a/frontend/src/app/gateways/page.tsx b/frontend/src/app/gateways/page.tsx
index b652e50a..ba1685db 100644
--- a/frontend/src/app/gateways/page.tsx
+++ b/frontend/src/app/gateways/page.tsx
@@ -26,6 +26,7 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
+import { TableEmptyStateRow, TableLoadingRow } from "@/components/ui/table-state";
import { ApiError } from "@/api/mutator";
import {
@@ -246,11 +247,7 @@ export default function GatewaysPage() {
{gatewaysQuery.isLoading ? (
-
- |
- Loading…
- |
-
+
) : table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row) => (
@@ -265,49 +262,27 @@ export default function GatewaysPage() {
))
) : (
-
-
-
-
-
- No gateways yet
-
-
- Create your first gateway to connect boards and start
- managing your OpenClaw connections.
-
-
- Create your first gateway
-
-
- |
-
+
+
+
+
+ }
+ title="No gateways yet"
+ description="Create your first gateway to connect boards and start managing your OpenClaw connections."
+ actionHref="/gateways/new"
+ actionLabel="Create your first gateway"
+ />
)}
diff --git a/frontend/src/components/ui/table-state.tsx b/frontend/src/components/ui/table-state.tsx
new file mode 100644
index 00000000..eb762fab
--- /dev/null
+++ b/frontend/src/components/ui/table-state.tsx
@@ -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 (
+
+ |
+ {label}
+ |
+
+ );
+}
+
+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 (
+
+
+
+ {icon}
+ {title}
+ {description}
+ {actionHref && actionLabel ? (
+
+ {actionLabel}
+
+ ) : null}
+
+ |
+
+ );
+}