From 5ea9719c1301196da5a9bb5837199f2b11e1b2c6 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Sun, 8 Feb 2026 23:58:55 +0530 Subject: [PATCH] feat: introduce DashboardPageLayout component to streamline page structure and improve layout consistency --- frontend/src/app/agents/new/page.tsx | 60 ++-- frontend/src/app/agents/page.tsx | 243 +++++++--------- .../src/app/boards/[boardId]/edit/page.tsx | 65 ++--- frontend/src/app/boards/new/page.tsx | 228 +++++++-------- .../app/gateways/[gatewayId]/edit/page.tsx | 132 ++++----- .../src/app/gateways/[gatewayId]/page.tsx | 99 +++---- frontend/src/app/gateways/new/page.tsx | 126 ++++----- frontend/src/app/gateways/page.tsx | 265 ++++++++---------- .../templates/DashboardPageLayout.tsx | 117 ++++++++ 9 files changed, 631 insertions(+), 704 deletions(-) create mode 100644 frontend/src/components/templates/DashboardPageLayout.tsx diff --git a/frontend/src/app/agents/new/page.tsx b/frontend/src/app/agents/new/page.tsx index 65375dd..930dd28 100644 --- a/frontend/src/app/agents/new/page.tsx +++ b/frontend/src/app/agents/new/page.tsx @@ -5,7 +5,7 @@ export const dynamic = "force-dynamic"; import { useState } from "react"; import { useRouter } from "next/navigation"; -import { SignedIn, SignedOut, useAuth } from "@/auth/clerk"; +import { useAuth } from "@/auth/clerk"; import { ApiError } from "@/api/mutator"; import { @@ -15,10 +15,7 @@ import { import { useCreateAgentApiV1AgentsPost } from "@/api/generated/agents/agents"; import { useOrganizationMembership } from "@/lib/use-organization-membership"; import type { BoardRead } from "@/api/generated/model"; -import { AdminOnlyNotice } from "@/components/auth/AdminOnlyNotice"; -import { SignedOutPanel } from "@/components/auth/SignedOutPanel"; -import { DashboardSidebar } from "@/components/organisms/DashboardSidebar"; -import { DashboardShell } from "@/components/templates/DashboardShell"; +import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import SearchableSelect, { @@ -155,36 +152,21 @@ export default function NewAgentPage() { }; return ( - - - - - - -
-
-
-

- Create agent -

-

- Agents start in provisioning until they check in. -

-
-
- -
- {!isAdmin ? ( - - ) : ( -
+ +

Basic configuration @@ -372,11 +354,7 @@ export default function NewAgentPage() { Back to agents

- - )} -
-
-
-
+ + ); } diff --git a/frontend/src/app/agents/page.tsx b/frontend/src/app/agents/page.tsx index f54363b..effa282 100644 --- a/frontend/src/app/agents/page.tsx +++ b/frontend/src/app/agents/page.tsx @@ -6,7 +6,7 @@ import { useMemo, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import { SignedIn, SignedOut, useAuth } from "@/auth/clerk"; +import { useAuth } from "@/auth/clerk"; import { type ColumnDef, type SortingState, @@ -18,8 +18,7 @@ import { import { useQueryClient } from "@tanstack/react-query"; import { StatusPill } from "@/components/atoms/StatusPill"; -import { DashboardSidebar } from "@/components/organisms/DashboardSidebar"; -import { DashboardShell } from "@/components/templates/DashboardShell"; +import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout"; import { Button, buttonVariants } from "@/components/ui/button"; import { Dialog, @@ -44,8 +43,6 @@ import { } from "@/api/generated/boards/boards"; import { useOrganizationMembership } from "@/lib/use-organization-membership"; import type { AgentRead } from "@/api/generated/model"; -import { AdminOnlyNotice } from "@/components/auth/AdminOnlyNotice"; -import { SignedOutPanel } from "@/components/auth/SignedOutPanel"; const parseTimestamp = (value?: string | null) => { if (!value) return null; @@ -289,138 +286,112 @@ export default function AgentsPage() { }); return ( - - - - - - -
-
-
-
-
-

- Agents -

-

- {agents.length} agent{agents.length === 1 ? "" : "s"} total. -

-
- {agents.length > 0 ? ( - - ) : null} -
-
+ <> + 0 ? ( + + ) : null + } + isAdmin={isAdmin} + adminOnlyMessage="Only organization owners and admins can access agents." + stickyHeader + > +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {agentsQuery.isLoading ? ( + + + + ) : table.getRowModel().rows.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + )) + ) : ( + + + + )} + +
+ {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} +
+ Loading… +
+ {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} +
+
+
+ + + + + + +
+

+ No agents yet +

+

+ Create your first agent to start executing tasks on + this board. +

+ + Create your first agent + +
+
+
-
- {!isAdmin ? ( - - ) : ( - <> -
-
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - ))} - - ))} - - - {agentsQuery.isLoading ? ( - - - - ) : table.getRowModel().rows.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - ))} - - )) - ) : ( - - - - )} - -
- {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} -
- - Loading… - -
- {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} -
-
-
- - - - - - -
-

- No agents yet -

-

- Create your first agent to start executing - tasks on this board. -

- - Create your first agent - -
-
-
-
- - {agentsQuery.error ? ( -

- {agentsQuery.error.message} -

- ) : null} - - )} -
-
-
+ {agentsQuery.error ? ( +

{agentsQuery.error.message}

+ ) : null} + -
+ ); } diff --git a/frontend/src/app/boards/[boardId]/edit/page.tsx b/frontend/src/app/boards/[boardId]/edit/page.tsx index ad87618..4142d58 100644 --- a/frontend/src/app/boards/[boardId]/edit/page.tsx +++ b/frontend/src/app/boards/[boardId]/edit/page.tsx @@ -5,7 +5,7 @@ export const dynamic = "force-dynamic"; import { useEffect, useMemo, useRef, useState } from "react"; import { useParams, useRouter, useSearchParams } from "next/navigation"; -import { SignedIn, SignedOut, useAuth } from "@/auth/clerk"; +import { useAuth } from "@/auth/clerk"; import { X } from "lucide-react"; import { ApiError } from "@/api/mutator"; @@ -29,10 +29,7 @@ import type { BoardUpdate, } from "@/api/generated/model"; import { BoardOnboardingChat } from "@/components/BoardOnboardingChat"; -import { AdminOnlyNotice } from "@/components/auth/AdminOnlyNotice"; -import { SignedOutPanel } from "@/components/auth/SignedOutPanel"; -import { DashboardSidebar } from "@/components/organisms/DashboardSidebar"; -import { DashboardShell } from "@/components/templates/DashboardShell"; +import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout"; import { Button } from "@/components/ui/button"; import { Dialog, DialogClose, DialogContent } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; @@ -293,37 +290,23 @@ export default function EditBoardPage() { return ( <> - - - - - - -
-
-
-

- Edit board -

-

- Update board settings and gateway. -

-
-
- -
- {!isAdmin ? ( - - ) : ( -
-
+ +
+ {resolvedBoardType !== "general" && baseBoard && !(baseBoard.goal_confirmed ?? false) ? ( @@ -496,13 +479,9 @@ export default function EditBoardPage() { {isLoading ? "Saving…" : "Save changes"}
- -
- )} -
-
-
-
+ + + - - - - - -
-
-
-

- Create board -

-

- Boards organize tasks and agents by mission context. -

+ +
+
+
+
+ + setName(event.target.value)} + placeholder="e.g. Release operations" + disabled={isLoading} + /> +
+
+ +
-
- {!isAdmin ? ( - - ) : ( - -
-
-
- - setName(event.target.value)} - placeholder="e.g. Release operations" - disabled={isLoading} - /> -
-
- - -
-
- -
-
- - -

- Optional. Groups increase cross-board visibility. -

-
-
-
- - {gateways.length === 0 ? ( -
-

- No gateways available. Create one in{" "} - - Gateways - {" "} - to continue. -

-
- ) : null} - - {errorMessage ? ( -

{errorMessage}

- ) : null} - -
- - -
- - )} +
+
+ + +

+ Optional. Groups increase cross-board visibility. +

+
-
-
- + + + {gateways.length === 0 ? ( +
+

+ No gateways available. Create one in{" "} + + Gateways + {" "} + to continue. +

+
+ ) : null} + + {errorMessage ?

{errorMessage}

: null} + +
+ + +
+ + ); } diff --git a/frontend/src/app/gateways/[gatewayId]/edit/page.tsx b/frontend/src/app/gateways/[gatewayId]/edit/page.tsx index 850a42f..6e7baaf 100644 --- a/frontend/src/app/gateways/[gatewayId]/edit/page.tsx +++ b/frontend/src/app/gateways/[gatewayId]/edit/page.tsx @@ -5,7 +5,7 @@ export const dynamic = "force-dynamic"; import { useState } from "react"; import { useParams, useRouter } from "next/navigation"; -import { SignedIn, SignedOut, useAuth } from "@/auth/clerk"; +import { useAuth } from "@/auth/clerk"; import { ApiError } from "@/api/mutator"; import { @@ -15,11 +15,8 @@ import { } from "@/api/generated/gateways/gateways"; import { useOrganizationMembership } from "@/lib/use-organization-membership"; import type { GatewayUpdate } from "@/api/generated/model"; -import { AdminOnlyNotice } from "@/components/auth/AdminOnlyNotice"; -import { SignedOutPanel } from "@/components/auth/SignedOutPanel"; import { GatewayForm } from "@/components/gateways/GatewayForm"; -import { DashboardSidebar } from "@/components/organisms/DashboardSidebar"; -import { DashboardShell } from "@/components/templates/DashboardShell"; +import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout"; import { DEFAULT_MAIN_SESSION_KEY, DEFAULT_WORKSPACE_ROOT, @@ -164,76 +161,59 @@ export default function EditGatewayPage() { }; return ( - - - - - - -
-
-
-

- {resolvedName.trim() - ? `Edit gateway — ${resolvedName.trim()}` - : "Edit gateway"} -

-

- Update connection settings for this OpenClaw gateway. -

-
-
- -
- {!isAdmin ? ( - - ) : ( - router.push("/gateways")} - onRunGatewayCheck={runGatewayCheck} - onNameChange={setName} - onGatewayUrlChange={(next) => { - setGatewayUrl(next); - setGatewayUrlError(null); - setGatewayCheckStatus("idle"); - setGatewayCheckMessage(null); - }} - onGatewayTokenChange={(next) => { - setGatewayToken(next); - setGatewayCheckStatus("idle"); - setGatewayCheckMessage(null); - }} - onMainSessionKeyChange={(next) => { - setMainSessionKey(next); - setGatewayCheckStatus("idle"); - setGatewayCheckMessage(null); - }} - onWorkspaceRootChange={setWorkspaceRoot} - /> - )} -
-
-
-
+ + router.push("/gateways")} + onRunGatewayCheck={runGatewayCheck} + onNameChange={setName} + onGatewayUrlChange={(next) => { + setGatewayUrl(next); + setGatewayUrlError(null); + setGatewayCheckStatus("idle"); + setGatewayCheckMessage(null); + }} + onGatewayTokenChange={(next) => { + setGatewayToken(next); + setGatewayCheckStatus("idle"); + setGatewayCheckMessage(null); + }} + onMainSessionKeyChange={(next) => { + setMainSessionKey(next); + setGatewayCheckStatus("idle"); + setGatewayCheckMessage(null); + }} + onWorkspaceRootChange={setWorkspaceRoot} + /> + ); } diff --git a/frontend/src/app/gateways/[gatewayId]/page.tsx b/frontend/src/app/gateways/[gatewayId]/page.tsx index c74e23e..0ba9017 100644 --- a/frontend/src/app/gateways/[gatewayId]/page.tsx +++ b/frontend/src/app/gateways/[gatewayId]/page.tsx @@ -5,7 +5,7 @@ export const dynamic = "force-dynamic"; import { useMemo } from "react"; import { useParams, useRouter } from "next/navigation"; -import { SignedIn, SignedOut, useAuth } from "@/auth/clerk"; +import { useAuth } from "@/auth/clerk"; import { ApiError } from "@/api/mutator"; import { @@ -19,10 +19,7 @@ import { useListAgentsApiV1AgentsGet, } from "@/api/generated/agents/agents"; import { useOrganizationMembership } from "@/lib/use-organization-membership"; -import { AdminOnlyNotice } from "@/components/auth/AdminOnlyNotice"; -import { SignedOutPanel } from "@/components/auth/SignedOutPanel"; -import { DashboardSidebar } from "@/components/organisms/DashboardSidebar"; -import { DashboardShell } from "@/components/templates/DashboardShell"; +import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout"; import { Button } from "@/components/ui/button"; const formatTimestamp = (value?: string | null) => { @@ -113,57 +110,38 @@ export default function GatewayDetailPage() { ); return ( - - - - - - -
-
-
-
-

- {title} -

-

- Gateway configuration and connection details. -

-
-
- - {isAdmin && gatewayId ? ( - - ) : null} -
-
-
- -
- {!isAdmin ? ( - - ) : gatewayQuery.isLoading ? ( -
- Loading gateway… -
- ) : gatewayQuery.error ? ( -
- {gatewayQuery.error.message} -
- ) : gateway ? ( -
+ + + {isAdmin && gatewayId ? ( + + ) : null} +
+ } + isAdmin={isAdmin} + adminOnlyMessage="Only organization owners and admins can access gateways." + > + {gatewayQuery.isLoading ? ( +
+ Loading gateway… +
+ ) : gatewayQuery.error ? ( +
+ {gatewayQuery.error.message} +
+ ) : gateway ? ( +
@@ -312,11 +290,8 @@ export default function GatewayDetailPage() {
-
- ) : null} -
-
-
-
+ + ) : null} + ); } diff --git a/frontend/src/app/gateways/new/page.tsx b/frontend/src/app/gateways/new/page.tsx index e264d53..dbd66ca 100644 --- a/frontend/src/app/gateways/new/page.tsx +++ b/frontend/src/app/gateways/new/page.tsx @@ -5,16 +5,13 @@ export const dynamic = "force-dynamic"; import { useState } from "react"; import { useRouter } from "next/navigation"; -import { SignedIn, SignedOut, useAuth } from "@/auth/clerk"; +import { useAuth } from "@/auth/clerk"; import { ApiError } from "@/api/mutator"; import { useCreateGatewayApiV1GatewaysPost } from "@/api/generated/gateways/gateways"; import { useOrganizationMembership } from "@/lib/use-organization-membership"; -import { AdminOnlyNotice } from "@/components/auth/AdminOnlyNotice"; -import { SignedOutPanel } from "@/components/auth/SignedOutPanel"; import { GatewayForm } from "@/components/gateways/GatewayForm"; -import { DashboardSidebar } from "@/components/organisms/DashboardSidebar"; -import { DashboardShell } from "@/components/templates/DashboardShell"; +import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout"; import { DEFAULT_MAIN_SESSION_KEY, DEFAULT_WORKSPACE_ROOT, @@ -125,74 +122,55 @@ export default function NewGatewayPage() { }; return ( - - - - - - -
-
-
-

- Create gateway -

-

- Configure an OpenClaw gateway for mission control. -

-
-
- -
- {!isAdmin ? ( - - ) : ( - router.push("/gateways")} - onRunGatewayCheck={runGatewayCheck} - onNameChange={setName} - onGatewayUrlChange={(next) => { - setGatewayUrl(next); - setGatewayUrlError(null); - setGatewayCheckStatus("idle"); - setGatewayCheckMessage(null); - }} - onGatewayTokenChange={(next) => { - setGatewayToken(next); - setGatewayCheckStatus("idle"); - setGatewayCheckMessage(null); - }} - onMainSessionKeyChange={(next) => { - setMainSessionKey(next); - setGatewayCheckStatus("idle"); - setGatewayCheckMessage(null); - }} - onWorkspaceRootChange={setWorkspaceRoot} - /> - )} -
-
-
-
+ + router.push("/gateways")} + onRunGatewayCheck={runGatewayCheck} + onNameChange={setName} + onGatewayUrlChange={(next) => { + setGatewayUrl(next); + setGatewayUrlError(null); + setGatewayCheckStatus("idle"); + setGatewayCheckMessage(null); + }} + onGatewayTokenChange={(next) => { + setGatewayToken(next); + setGatewayCheckStatus("idle"); + setGatewayCheckMessage(null); + }} + onMainSessionKeyChange={(next) => { + setMainSessionKey(next); + setGatewayCheckStatus("idle"); + setGatewayCheckMessage(null); + }} + onWorkspaceRootChange={setWorkspaceRoot} + /> + ); } diff --git a/frontend/src/app/gateways/page.tsx b/frontend/src/app/gateways/page.tsx index 0c340f9..ecb56a1 100644 --- a/frontend/src/app/gateways/page.tsx +++ b/frontend/src/app/gateways/page.tsx @@ -5,7 +5,7 @@ export const dynamic = "force-dynamic"; import { useMemo, useState } from "react"; import Link from "next/link"; -import { SignedIn, SignedOut, useAuth } from "@/auth/clerk"; +import { useAuth } from "@/auth/clerk"; import { type ColumnDef, type SortingState, @@ -16,8 +16,7 @@ import { } from "@tanstack/react-table"; import { useQueryClient } from "@tanstack/react-query"; -import { DashboardSidebar } from "@/components/organisms/DashboardSidebar"; -import { DashboardShell } from "@/components/templates/DashboardShell"; +import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout"; import { Button, buttonVariants } from "@/components/ui/button"; import { Dialog, @@ -37,8 +36,6 @@ import { } from "@/api/generated/gateways/gateways"; import { useOrganizationMembership } from "@/lib/use-organization-membership"; import type { GatewayRead } from "@/api/generated/model"; -import { AdminOnlyNotice } from "@/components/auth/AdminOnlyNotice"; -import { SignedOutPanel } from "@/components/auth/SignedOutPanel"; const truncate = (value?: string | null, max = 24) => { if (!value) return "—"; @@ -220,148 +217,124 @@ export default function GatewaysPage() { }); return ( - - - - - - -
-
-
-
-
-

- Gateways -

-

- Manage OpenClaw gateway connections used by boards -

-
- {isAdmin && gateways.length > 0 ? ( - - Create gateway - - ) : null} -
-
-
- -
- {!isAdmin ? ( - - ) : ( - <> -
-
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - ))} - - ))} - - - {gatewaysQuery.isLoading ? ( - - - - ) : table.getRowModel().rows.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - ))} - - )) - ) : ( - - - + <> + 0 ? ( + + Create gateway + + ) : null + } + isAdmin={isAdmin} + adminOnlyMessage="Only organization owners and admins can access gateways." + stickyHeader + > +
+
+
- {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} -
- - Loading… - -
- {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} -
-
-
- - - - -
-

- No gateways yet -

-

- Create your first gateway to connect boards - and start managing your OpenClaw connections. -

- - Create your first gateway - -
-
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {gatewaysQuery.isLoading ? ( + + + + ) : table.getRowModel().rows.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + -
+ {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} +
+ Loading… +
+ {flexRender( + cell.column.columnDef.cell, + cell.getContext(), )} -
-
-
+ + ))} + + )) + ) : ( + + +
+
+ + + + +
+

+ No gateways yet +

+

+ Create your first gateway to connect boards and start + managing your OpenClaw connections. +

+ + Create your first gateway + +
+ + + )} + + +
+ - {gatewaysQuery.error ? ( -

- {gatewaysQuery.error.message} -

- ) : null} - - )} - -
-
+ {gatewaysQuery.error ? ( +

{gatewaysQuery.error.message}

+ ) : null} +
- + ); } diff --git a/frontend/src/components/templates/DashboardPageLayout.tsx b/frontend/src/components/templates/DashboardPageLayout.tsx new file mode 100644 index 0000000..e29a937 --- /dev/null +++ b/frontend/src/components/templates/DashboardPageLayout.tsx @@ -0,0 +1,117 @@ +import type { ReactNode, Ref } from "react"; + +import { SignedIn, SignedOut } from "@/auth/clerk"; + +import { AdminOnlyNotice } from "@/components/auth/AdminOnlyNotice"; +import { SignedOutPanel } from "@/components/auth/SignedOutPanel"; +import { DashboardSidebar } from "@/components/organisms/DashboardSidebar"; +import { cn } from "@/lib/utils"; + +import { DashboardShell } from "./DashboardShell"; + +type SignedOutConfig = { + message: string; + forceRedirectUrl: string; + signUpForceRedirectUrl?: string; + mode?: "modal" | "redirect"; + buttonLabel?: string; + buttonTestId?: string; +}; + +type DashboardPageLayoutProps = { + signedOut: SignedOutConfig; + title: ReactNode; + description?: ReactNode; + headerActions?: ReactNode; + children: ReactNode; + isAdmin?: boolean; + adminOnlyMessage?: string; + stickyHeader?: boolean; + mainClassName?: string; + headerClassName?: string; + contentClassName?: string; + mainRef?: Ref; +}; + +export function DashboardPageLayout({ + signedOut, + title, + description, + headerActions, + children, + isAdmin, + adminOnlyMessage, + stickyHeader = false, + mainClassName, + headerClassName, + contentClassName, + mainRef, +}: DashboardPageLayoutProps) { + const showAdminOnlyNotice = + typeof isAdmin === "boolean" && Boolean(adminOnlyMessage) && !isAdmin; + + return ( + + + + + + +
+
+
+ {headerActions ? ( +
+
+

+ {title} +

+ {description ? ( +

+ {description} +

+ ) : null} +
+ {headerActions} +
+ ) : ( +
+

+ {title} +

+ {description ? ( +

{description}

+ ) : null} +
+ )} +
+
+ +
+ {showAdminOnlyNotice ? ( + + ) : ( + children + )} +
+
+
+
+ ); +}