mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-10-15 05:11:26 +00:00
refactor: simplify SwarmMonitorCard logic and update dashboard structure
- Refactored SwarmMonitorCard to use optional chaining for node data handling. - Consolidated loading and error states into a single conditional rendering block. - Updated dashboard layout to include tabs for containers and swarm, enhancing navigation. - Removed deprecated network-related code from the dashboard.
This commit is contained in:
@@ -27,50 +27,16 @@ export default function SwarmMonitorCard({ serverId }: Props) {
|
||||
serverId,
|
||||
});
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="w-full max-w-7xl mx-auto">
|
||||
<div className="mb-6 border min-h-[55vh] flex rounded-lg h-full items-center justify-center text-muted-foreground">
|
||||
{/* <div className="flex items-center justify-center h-full text-muted-foreground"> */}
|
||||
|
||||
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[55vh]">
|
||||
<span>Loading...</span>
|
||||
<Loader2 className="animate-spin size-4" />
|
||||
</div>
|
||||
{/* </div> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!nodes) {
|
||||
return (
|
||||
<div className="w-full max-w-7xl mx-auto">
|
||||
<div className="mb-6 border min-h-[55vh] flex justify-center items-center rounded-lg h-full">
|
||||
<div className="flex items-center justify-center h-full text-destructive">
|
||||
<span>Failed to load data</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const totalNodes = nodes.length;
|
||||
const activeNodesCount = nodes.filter(
|
||||
(node) => node.Status === "Ready",
|
||||
).length;
|
||||
const managerNodesCount = nodes.filter(
|
||||
(node) =>
|
||||
node.ManagerStatus === "Leader" || node.ManagerStatus === "Reachable",
|
||||
).length;
|
||||
const activeNodes = nodes.filter((node) => node.Status === "Ready");
|
||||
const managerNodes = nodes.filter(
|
||||
(node) =>
|
||||
node.ManagerStatus === "Leader" || node.ManagerStatus === "Reachable",
|
||||
);
|
||||
const totalNodes = nodes?.length ?? 0;
|
||||
const activeNodes = nodes?.filter((node) => node.Status === "Ready") ?? [];
|
||||
const managerNodes =
|
||||
nodes?.filter(
|
||||
(node) =>
|
||||
node.ManagerStatus === "Leader" || node.ManagerStatus === "Reachable",
|
||||
) ?? [];
|
||||
|
||||
return (
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
|
||||
<div className="rounded-xl bg-background shadow-md p-6 flex flex-col gap-4">
|
||||
<header className="flex items-center flex-wrap gap-4 justify-between">
|
||||
<div className="space-y-1">
|
||||
@@ -94,93 +60,114 @@ export default function SwarmMonitorCard({ serverId }: Props) {
|
||||
)}
|
||||
</header>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-3">
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Nodes</CardTitle>
|
||||
<div className="p-2 bg-emerald-600/20 text-emerald-600 rounded-md">
|
||||
<Server className="h-4 w-4 text-muted-foreground dark:text-emerald-600" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{totalNodes}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{isPending ? (
|
||||
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[45vh]">
|
||||
<span>Loading...</span>
|
||||
<Loader2 className="animate-spin size-4" />
|
||||
</div>
|
||||
) : !nodes ? (
|
||||
<div className="flex items-center justify-center min-h-[45vh] text-destructive">
|
||||
<span>Failed to load data</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid gap-6 lg:grid-cols-3">
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
Total Nodes
|
||||
</CardTitle>
|
||||
<div className="p-2 bg-emerald-600/20 text-emerald-600 rounded-md">
|
||||
<Server className="h-4 w-4 text-muted-foreground dark:text-emerald-600" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{totalNodes}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
Active Nodes
|
||||
</CardTitle>
|
||||
<Badge variant="green">Online</Badge>
|
||||
</div>
|
||||
<div className="p-2 bg-emerald-600/20 text-emerald-600 rounded-md">
|
||||
<Activity className="h-4 w-4 text-muted-foreground dark:text-emerald-600" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="text-2xl font-bold">
|
||||
{activeNodesCount} / {totalNodes}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="max-h-48 overflow-y-auto">
|
||||
{activeNodes.map((node) => (
|
||||
<div key={node.ID} className="flex items-center gap-2">
|
||||
{node.Hostname}
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
Active Nodes
|
||||
</CardTitle>
|
||||
<Badge variant="green">Online</Badge>
|
||||
</div>
|
||||
<div className="p-2 bg-emerald-600/20 text-emerald-600 rounded-md">
|
||||
<Activity className="h-4 w-4 text-muted-foreground dark:text-emerald-600" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="text-2xl font-bold">
|
||||
{activeNodes.length} / {totalNodes}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
Manager Nodes
|
||||
</CardTitle>
|
||||
<Badge variant="green">Online</Badge>
|
||||
</div>
|
||||
<div className="p-2 bg-emerald-600/20 text-emerald-600 rounded-md">
|
||||
<Monitor className="h-4 w-4 text-muted-foreground dark:text-emerald-600" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="text-2xl font-bold">
|
||||
{managerNodesCount} / {totalNodes}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="max-h-48 overflow-y-auto">
|
||||
{managerNodes.map((node) => (
|
||||
<div key={node.ID} className="flex items-center gap-2">
|
||||
{node.Hostname}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="max-h-48 overflow-y-auto">
|
||||
{activeNodes.map((node) => (
|
||||
<div
|
||||
key={node.ID}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{node.Hostname}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 2xl:grid-cols-3 gap-4">
|
||||
{nodes.map((node) => (
|
||||
<NodeCard key={node.ID} node={node} serverId={serverId} />
|
||||
))}
|
||||
</div>
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
Manager Nodes
|
||||
</CardTitle>
|
||||
<Badge variant="green">Online</Badge>
|
||||
</div>
|
||||
<div className="p-2 bg-emerald-600/20 text-emerald-600 rounded-md">
|
||||
<Monitor className="h-4 w-4 text-muted-foreground dark:text-emerald-600" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="text-2xl font-bold">
|
||||
{managerNodes.length} / {totalNodes}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="max-h-48 overflow-y-auto">
|
||||
{managerNodes.map((node) => (
|
||||
<div
|
||||
key={node.ID}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{node.Hostname}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 2xl:grid-cols-3 gap-4">
|
||||
{nodes.map((node) => (
|
||||
<NodeCard key={node.ID} node={node} serverId={serverId} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -25,10 +25,8 @@ import {
|
||||
Loader2,
|
||||
LogIn,
|
||||
type LucideIcon,
|
||||
Network,
|
||||
Package,
|
||||
Palette,
|
||||
PieChart,
|
||||
Rocket,
|
||||
Server,
|
||||
ShieldCheck,
|
||||
@@ -202,28 +200,6 @@ const MENU: Menu = {
|
||||
// Only enabled for users with access to Docker
|
||||
isEnabled: ({ permissions }) => !!permissions?.docker.read,
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
title: "Swarm",
|
||||
url: "/dashboard/swarm",
|
||||
icon: PieChart,
|
||||
// Only enabled for users with access to Docker
|
||||
isEnabled: ({ permissions }) => !!permissions?.docker.read,
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
title: "Networks",
|
||||
url: "/dashboard/networks",
|
||||
icon: Network,
|
||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!(
|
||||
(auth?.role === "owner" ||
|
||||
auth?.role === "admin" ||
|
||||
auth?.canAccessToDocker) &&
|
||||
!isCloud
|
||||
),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
title: "Requests",
|
||||
|
||||
@@ -1,17 +1,79 @@
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import type { ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
import { ShowContainers } from "@/components/dashboard/docker/show/show-containers";
|
||||
import { ShowNetworks } from "@/components/dashboard/networks/show-networks";
|
||||
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";
|
||||
import { ServerFilter } from "@/components/shared/server-filter";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const DEFAULT_TAB = "containers";
|
||||
|
||||
const Dashboard = () => {
|
||||
const router = useRouter();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
const queryTab =
|
||||
typeof router.query.tab === "string" ? router.query.tab : DEFAULT_TAB;
|
||||
const activeTab = isCloud && queryTab === "networks" ? DEFAULT_TAB : queryTab;
|
||||
|
||||
const setTab = (value: string) => {
|
||||
const { tab: _current, ...query } = router.query;
|
||||
router.replace(
|
||||
{
|
||||
pathname: router.pathname,
|
||||
query: value === DEFAULT_TAB ? query : { ...query, tab: value },
|
||||
},
|
||||
undefined,
|
||||
{ shallow: true },
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ServerFilter>
|
||||
{(serverId) => <ShowContainers serverId={serverId} />}
|
||||
{(serverId) => (
|
||||
<Tabs value={activeTab} onValueChange={setTab}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="containers">Containers</TabsTrigger>
|
||||
<TabsTrigger value="swarm">Swarm</TabsTrigger>
|
||||
{!isCloud && <TabsTrigger value="networks">Networks</TabsTrigger>}
|
||||
</TabsList>
|
||||
<TabsContent value="containers">
|
||||
<ShowContainers serverId={serverId} />
|
||||
</TabsContent>
|
||||
<TabsContent value="swarm">
|
||||
<Tabs defaultValue="overview">
|
||||
<TabsList>
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="containers">Containers</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="overview">
|
||||
<SwarmMonitorCard serverId={serverId} />
|
||||
</TabsContent>
|
||||
<TabsContent value="containers">
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
|
||||
<div className="rounded-xl bg-background shadow-md p-6">
|
||||
<ShowSwarmContainers serverId={serverId} />
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</TabsContent>
|
||||
{!isCloud && (
|
||||
<TabsContent value="networks">
|
||||
<ShowNetworks serverId={serverId} />
|
||||
</TabsContent>
|
||||
)}
|
||||
</Tabs>
|
||||
)}
|
||||
</ServerFilter>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,89 +1,21 @@
|
||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import type { ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
import { ShowNetworks } from "@/components/dashboard/networks/show-networks";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { ServerFilter } from "@/components/shared/server-filter";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
|
||||
const Dashboard = () => {
|
||||
return (
|
||||
<ServerFilter>
|
||||
{(serverId) => <ShowNetworks serverId={serverId} />}
|
||||
</ServerFilter>
|
||||
);
|
||||
const Networks = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
export default Networks;
|
||||
|
||||
Dashboard.getLayout = (page: ReactElement) => {
|
||||
return <DashboardLayout>{page}</DashboardLayout>;
|
||||
};
|
||||
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
||||
const serverId =
|
||||
typeof ctx.query.serverId === "string" ? ctx.query.serverId : undefined;
|
||||
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
if (IS_CLOUD) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
const { user, session } = await validateRequest(ctx.req);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
const { req } = ctx;
|
||||
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: ctx.res as any,
|
||||
db: null as any,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: `/dashboard/docker?tab=networks${
|
||||
serverId ? `&serverId=${encodeURIComponent(serverId)}` : ""
|
||||
}`,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
try {
|
||||
await helpers.project.all.prefetch();
|
||||
|
||||
if (user.role === "member") {
|
||||
const userR = await helpers.user.one.fetch({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (!userR?.canAccessToDocker) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
await helpers.network.all.prefetch({});
|
||||
|
||||
return {
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
props: {},
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,94 +1,21 @@
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import type { ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
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";
|
||||
import { ServerFilter } from "@/components/shared/server-filter";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
|
||||
const Dashboard = () => {
|
||||
return (
|
||||
<ServerFilter>
|
||||
{(serverId) => (
|
||||
<div className="space-y-4">
|
||||
<Tabs defaultValue="overview">
|
||||
<TabsList>
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="containers">Containers</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="overview">
|
||||
<SwarmMonitorCard serverId={serverId} />
|
||||
</TabsContent>
|
||||
<TabsContent value="containers">
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
|
||||
<div className="rounded-xl bg-background shadow-md p-6">
|
||||
<ShowSwarmContainers serverId={serverId} />
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)}
|
||||
</ServerFilter>
|
||||
);
|
||||
const Swarm = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
export default Swarm;
|
||||
|
||||
Dashboard.getLayout = (page: ReactElement) => {
|
||||
return <DashboardLayout>{page}</DashboardLayout>;
|
||||
};
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { user, session } = await validateRequest(ctx.req);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
const { req, res } = ctx;
|
||||
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
||||
const serverId =
|
||||
typeof ctx.query.serverId === "string" ? ctx.query.serverId : undefined;
|
||||
|
||||
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,
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: `/dashboard/docker?tab=swarm${
|
||||
serverId ? `&serverId=${encodeURIComponent(serverId)}` : ""
|
||||
}`,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
try {
|
||||
await helpers.project.all.prefetch();
|
||||
|
||||
const userPermissions = await helpers.user.getPermissions.fetch();
|
||||
|
||||
if (!userPermissions?.docker.read) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
props: {},
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user