refactor: move cluster nodes into docker dashboard swarm tab

- Add Nodes sub-tab under Docker > Swarm (visible with organization.update permission)
- Remove Cluster item from settings sidebar and delete /dashboard/settings/cluster page
- Point internal cluster links to /dashboard/docker?tab=swarm&subtab=nodes
- Make ShowNodes card full-width to match the other docker tabs
This commit is contained in:
Mauricio Siu 2026-08-11 01:55:42 -06:00
parent e237e34e14
commit 1617d07fbd
8 changed files with 48 additions and 78 deletions

View File

@ -187,7 +187,7 @@ export const ShowClusterSettings = ({ id, type }: Props) => {
To use a cluster feature, you need to configure at least
a registry first. Please, go to{" "}
<Link
href="/dashboard/settings/cluster"
href="/dashboard/docker?tab=swarm&subtab=nodes"
className="text-foreground"
>
Settings

View File

@ -58,7 +58,7 @@ export const ShowNodes = ({ serverId }: Props) => {
const haveAtLeastOneRegistry = !!(registry && registry?.length > 0);
return (
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
<div className="rounded-xl bg-background shadow-md ">
<CardHeader className="flex flex-row gap-2 justify-between w-full items-center flex-wrap">
<div className="flex flex-col gap-2">

View File

@ -35,7 +35,7 @@ export const DocLinks = () => (
<ExternalLink className="h-3 w-3" />
</a>
<Link
href="/dashboard/settings/cluster"
href="/dashboard/docker?tab=swarm&subtab=nodes"
className="text-xs text-primary underline underline-offset-4 inline-flex items-center gap-1"
>
Cluster Settings
@ -85,7 +85,7 @@ export const SwarmNotAvailable = ({
<li>
Check the{" "}
<Link
href="/dashboard/settings/cluster"
href="/dashboard/docker?tab=swarm&subtab=nodes"
className="text-primary underline underline-offset-4"
>
Cluster Settings
@ -130,7 +130,7 @@ export const ServicesError = ({
<li>
Network connectivity issues to a remote server &mdash; check{" "}
<Link
href="/dashboard/settings/cluster"
href="/dashboard/docker?tab=swarm&subtab=nodes"
className="text-primary underline underline-offset-4"
>
Cluster Settings
@ -183,7 +183,7 @@ export const NoServices = ({ nodeCount, onRefresh }: NoServicesProps) => (
Worker nodes need to pull images from a shared registry. Configure one
in{" "}
<Link
href="/dashboard/settings/cluster"
href="/dashboard/docker?tab=swarm&subtab=nodes"
className="text-primary underline underline-offset-4"
>
Cluster Settings
@ -255,7 +255,7 @@ export const NoRunningContainers = ({
<li>
Images can&apos;t be pulled on worker nodes &mdash; verify your{" "}
<Link
href="/dashboard/settings/cluster"
href="/dashboard/docker?tab=swarm&subtab=nodes"
className="text-primary underline underline-offset-4"
>
registry configuration

View File

@ -303,7 +303,7 @@ export const ShowSwarmContainers = ({ serverId }: Props) => {
<p className="mt-2 text-xs">
Manage nodes in{" "}
<Link
href="/dashboard/settings/cluster"
href="/dashboard/docker?tab=swarm&subtab=nodes"
className="underline underline-offset-4"
>
Cluster Settings

View File

@ -51,7 +51,9 @@ export default function SwarmMonitorCard({ serverId }: Props) {
{!serverId && (
<Button
onClick={() =>
window.location.replace("/dashboard/settings/cluster")
window.location.replace(
"/dashboard/docker?tab=swarm&subtab=nodes",
)
}
>
<Settings className="mr-2 h-4 w-4" />

View File

@ -373,14 +373,6 @@ const MENU: Menu = {
icon: ShieldCheck,
isEnabled: ({ permissions }) => !!permissions?.certificate.read,
},
{
isSingle: true,
title: "Cluster",
url: "/dashboard/settings/cluster",
icon: Boxes,
// Only enabled for admins
isEnabled: ({ permissions }) => !!permissions?.organization.update,
},
{
isSingle: true,
title: "Notifications",

View File

@ -7,6 +7,7 @@ import superjson from "superjson";
import { ShowContainers } from "@/components/dashboard/docker/show/show-containers";
import { ShowVolumes } from "@/components/dashboard/docker/volumes/show-volumes";
import { ShowNetworks } from "@/components/dashboard/networks/show-networks";
import { ShowNodes } from "@/components/dashboard/settings/cluster/nodes/show-nodes";
import { ShowSwarmContainers } from "@/components/dashboard/swarm/containers/show-swarm-containers";
import SwarmMonitorCard from "@/components/dashboard/swarm/monitoring-card";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
@ -17,17 +18,30 @@ import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
const DEFAULT_TAB = "containers";
const DEFAULT_SWARM_TAB = "overview";
const Dashboard = () => {
const router = useRouter();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: permissions } = api.user.getPermissions.useQuery();
const canManageCluster = !!permissions?.organization.update;
const queryTab =
typeof router.query.tab === "string" ? router.query.tab : DEFAULT_TAB;
const activeTab = isCloud && queryTab === "networks" ? DEFAULT_TAB : queryTab;
const querySwarmTab =
typeof router.query.subtab === "string"
? router.query.subtab
: DEFAULT_SWARM_TAB;
const activeSwarmTab =
!canManageCluster && querySwarmTab === "nodes"
? DEFAULT_SWARM_TAB
: querySwarmTab;
const setTab = (value: string) => {
const { tab: _current, ...query } = router.query;
const { tab: _current, subtab: _subtab, ...query } = router.query;
router.replace(
{
pathname: router.pathname,
@ -38,6 +52,19 @@ const Dashboard = () => {
);
};
const setSwarmTab = (value: string) => {
const { subtab: _current, ...query } = router.query;
router.replace(
{
pathname: router.pathname,
query:
value === DEFAULT_SWARM_TAB ? query : { ...query, subtab: value },
},
undefined,
{ shallow: true },
);
};
return (
<ServerFilter>
{(serverId) => (
@ -52,10 +79,13 @@ const Dashboard = () => {
<ShowContainers serverId={serverId} />
</TabsContent>
<TabsContent value="swarm">
<Tabs defaultValue="overview">
<Tabs value={activeSwarmTab} onValueChange={setSwarmTab}>
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="containers">Containers</TabsTrigger>
{canManageCluster && (
<TabsTrigger value="nodes">Nodes</TabsTrigger>
)}
</TabsList>
<TabsContent value="overview">
<SwarmMonitorCard serverId={serverId} />
@ -67,6 +97,11 @@ const Dashboard = () => {
</div>
</Card>
</TabsContent>
{canManageCluster && (
<TabsContent value="nodes">
<ShowNodes serverId={serverId} />
</TabsContent>
)}
</Tabs>
</TabsContent>
<TabsContent value="volumes">

View File

@ -1,59 +0,0 @@
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import type { ReactElement } from "react";
import superjson from "superjson";
import { ShowNodes } from "@/components/dashboard/settings/cluster/nodes/show-nodes";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { ServerFilter } from "@/components/shared/server-filter";
import { appRouter } from "@/server/api/root";
const Page = () => {
return (
<ServerFilter>
{(serverId) => (
<div className="flex flex-col gap-4 w-full">
<ShowNodes serverId={serverId} />
</div>
)}
</ServerFilter>
);
};
export default Page;
Page.getLayout = (page: ReactElement) => {
return <DashboardLayout metaName="Nodes">{page}</DashboardLayout>;
};
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { req, res } = ctx;
const { user, session } = await validateRequest(ctx.req);
if (!user || user.role === "member") {
return {
redirect: {
permanent: false,
destination: "/",
},
};
}
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
req: req as any,
res: res as any,
db: null as any,
session: session as any,
user: user as any,
},
transformer: superjson,
});
await helpers.user.get.prefetch();
return {
props: {
trpcState: helpers.dehydrate(),
},
};
}