feat: add TableLoadingRow and TableEmptyStateRow components for improved table state handling
This commit is contained in:
@@ -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,44 +307,29 @@ 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"
|
fill="none"
|
||||||
fill="none"
|
stroke="currentColor"
|
||||||
stroke="currentColor"
|
strokeWidth="1.5"
|
||||||
strokeWidth="1.5"
|
strokeLinecap="round"
|
||||||
strokeLinecap="round"
|
strokeLinejoin="round"
|
||||||
strokeLinejoin="round"
|
>
|
||||||
>
|
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
|
||||||
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
|
<circle cx="9" cy="7" r="4" />
|
||||||
<circle cx="9" cy="7" r="4" />
|
<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>
|
title="No agents yet"
|
||||||
<h3 className="mb-2 text-lg font-semibold text-slate-900">
|
description="Create your first agent to start executing tasks on this board."
|
||||||
No agents yet
|
actionHref="/agents/new"
|
||||||
</h3>
|
actionLabel="Create your first agent"
|
||||||
<p className="mb-6 max-w-md text-sm text-slate-500">
|
/>
|
||||||
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>
|
||||||
|
|||||||
@@ -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,45 +254,30 @@ 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"
|
fill="none"
|
||||||
fill="none"
|
stroke="currentColor"
|
||||||
stroke="currentColor"
|
strokeWidth="1.5"
|
||||||
strokeWidth="1.5"
|
strokeLinecap="round"
|
||||||
strokeLinecap="round"
|
strokeLinejoin="round"
|
||||||
strokeLinejoin="round"
|
>
|
||||||
>
|
<path d="M3 7h8" />
|
||||||
<path d="M3 7h8" />
|
<path d="M3 17h8" />
|
||||||
<path d="M3 17h8" />
|
<path d="M13 7h8" />
|
||||||
<path d="M13 7h8" />
|
<path d="M13 17h8" />
|
||||||
<path d="M13 17h8" />
|
<path d="M3 12h18" />
|
||||||
<path d="M3 12h18" />
|
</svg>
|
||||||
</svg>
|
}
|
||||||
</div>
|
title="No groups yet"
|
||||||
<h3 className="mb-2 text-lg font-semibold text-slate-900">
|
description="Create a board group to increase cross-board visibility for agents."
|
||||||
No groups yet
|
actionHref="/board-groups/new"
|
||||||
</h3>
|
actionLabel="Create your first group"
|
||||||
<p className="mb-6 max-w-md text-sm text-slate-500">
|
/>
|
||||||
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>
|
||||||
|
|||||||
@@ -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,44 +309,29 @@ 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"
|
fill="none"
|
||||||
fill="none"
|
stroke="currentColor"
|
||||||
stroke="currentColor"
|
strokeWidth="1.5"
|
||||||
strokeWidth="1.5"
|
strokeLinecap="round"
|
||||||
strokeLinecap="round"
|
strokeLinejoin="round"
|
||||||
strokeLinejoin="round"
|
>
|
||||||
>
|
<rect x="3" y="3" width="7" height="7" />
|
||||||
<rect x="3" y="3" width="7" height="7" />
|
<rect x="14" y="3" width="7" height="7" />
|
||||||
<rect x="14" y="3" width="7" height="7" />
|
<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>
|
title="No boards yet"
|
||||||
<h3 className="mb-2 text-lg font-semibold text-slate-900">
|
description="Create your first board to start routing tasks and monitoring work across agents."
|
||||||
No boards yet
|
actionHref="/boards/new"
|
||||||
</h3>
|
actionLabel="Create your first board"
|
||||||
<p className="mb-6 max-w-md text-sm text-slate-500">
|
/>
|
||||||
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>
|
||||||
|
|||||||
@@ -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,49 +262,27 @@ 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"
|
fill="none"
|
||||||
fill="none"
|
stroke="currentColor"
|
||||||
stroke="currentColor"
|
strokeWidth="1.5"
|
||||||
strokeWidth="1.5"
|
strokeLinecap="round"
|
||||||
strokeLinecap="round"
|
strokeLinejoin="round"
|
||||||
strokeLinejoin="round"
|
>
|
||||||
>
|
<rect x="2" y="7" width="20" height="14" rx="2" ry="2" />
|
||||||
<rect
|
<path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16" />
|
||||||
x="2"
|
</svg>
|
||||||
y="7"
|
}
|
||||||
width="20"
|
title="No gateways yet"
|
||||||
height="14"
|
description="Create your first gateway to connect boards and start managing your OpenClaw connections."
|
||||||
rx="2"
|
actionHref="/gateways/new"
|
||||||
ry="2"
|
actionLabel="Create your first gateway"
|
||||||
/>
|
/>
|
||||||
<path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h3 className="mb-2 text-lg font-semibold text-slate-900">
|
|
||||||
No gateways yet
|
|
||||||
</h3>
|
|
||||||
<p className="mb-6 max-w-md text-sm text-slate-500">
|
|
||||||
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>
|
||||||
|
|||||||
60
frontend/src/components/ui/table-state.tsx
Normal file
60
frontend/src/components/ui/table-state.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user