+
+
+ );
+}
diff --git a/apps/dashboard/components/process/ProcessClusterMetricRow.tsx b/apps/dashboard/components/process/ProcessClusterMetricRow.tsx
new file mode 100644
index 0000000..2719599
--- /dev/null
+++ b/apps/dashboard/components/process/ProcessClusterMetricRow.tsx
@@ -0,0 +1,82 @@
+import { Flex } from "@mantine/core";
+import { IProcess } from "@pm2.web/typings";
+import { IconCpu, IconDeviceSdCard, IconHistory } from "@tabler/icons-react";
+import ms from "ms";
+
+import { formatBytes } from "@/utils/format";
+import { trpc } from "@/utils/trpc";
+
+import ProcessGitMetric from "./ProcessGitMetric";
+import ProcessItemMetric from "./ProcessMetric";
+
+interface ProcessClusterMetricRowProps {
+ processes: IProcess[];
+ refetchInterval: number;
+ showMetric: boolean;
+}
+
+export default function ProcessClusterMetricRow({ processes, refetchInterval, showMetric }: ProcessClusterMetricRowProps) {
+ // Get stats for all processes in the cluster
+ const statsQueries = processes.map(process =>
+ trpc.process.getStat.useQuery(
+ { processId: process._id },
+ {
+ refetchInterval,
+ enabled: process.status === "online", // Only fetch stats for online processes
+ }
+ )
+ );
+
+ // Calculate aggregated metrics
+ const aggregatedMetrics = () => {
+ if (!showMetric) return { memory: 0, cpu: 0, uptime: 0 };
+
+ let totalMemory = 0;
+ let totalCpu = 0;
+ let oldestUptime = 0;
+
+ statsQueries.forEach((query, index) => {
+ if (query.data && processes[index].status === "online") {
+ totalMemory += query.data.memory || 0;
+ totalCpu += query.data.cpu || 0;
+
+ // For uptime, we want the oldest (highest value) among online processes
+ if (query.data.uptime && query.data.uptime > oldestUptime) {
+ oldestUptime = query.data.uptime;
+ }
+ }
+ });
+
+ return {
+ memory: totalMemory,
+ cpu: totalCpu,
+ uptime: oldestUptime,
+ };
+ };
+
+ const metrics = aggregatedMetrics();
+
+ // Get primary process for Git versioning info (prefer online process)
+ const primaryProcess = processes.find(p => p.status === "online") || processes[0];
+
+ return (
+
+ {primaryProcess?.versioning?.url && }
+
+
+
+
+ );
+}
diff --git a/apps/dashboard/pages/process.tsx b/apps/dashboard/pages/process.tsx
index 14fc278..705a57d 100644
--- a/apps/dashboard/pages/process.tsx
+++ b/apps/dashboard/pages/process.tsx
@@ -1,20 +1,106 @@
-import { Flex } from "@mantine/core";
-import { ISetting } from "@pm2.web/typings";
+import { Flex, Switch, Paper, Text, Group, Tooltip } from "@mantine/core";
+import { ISetting, IProcess } from "@pm2.web/typings";
import { InferGetServerSidePropsType } from "next";
import Head from "next/head";
+import { useState, useEffect } from "react";
import { SelectedProvider, useSelected } from "@/components/context/SelectedProvider";
import { Dashboard } from "@/components/layouts/Dashboard";
import ProcessItem from "@/components/process/ProcessItem";
+import ProcessCluster from "@/components/process/ProcessCluster";
import { getServerSideHelpers } from "@/server/helpers";
import { trpc } from "@/utils/trpc";
function Process({ settings }: { settings: ISetting }) {
const { selectedProcesses } = useSelected();
+ const [clusterViewEnabled, setClusterViewEnabled] = useState(true);
+
+ // Load cluster view preference from localStorage on mount
+ useEffect(() => {
+ const savedPreference = localStorage.getItem('pm2-cluster-view-enabled');
+ if (savedPreference !== null) {
+ setClusterViewEnabled(JSON.parse(savedPreference));
+ }
+ }, []);
+
+ // Save cluster view preference to localStorage when it changes
+ const handleToggleChange = (event: React.ChangeEvent) => {
+ const newValue = event.currentTarget.checked;
+ setClusterViewEnabled(newValue);
+ localStorage.setItem('pm2-cluster-view-enabled', JSON.stringify(newValue));
+ };
+
+ // Group processes by name (cluster support) - only if cluster view is enabled
+ const processGroups = clusterViewEnabled
+ ? selectedProcesses?.reduce((groups: { [key: string]: IProcess[] }, process) => {
+ const key = process.name;
+ if (!groups[key]) {
+ groups[key] = [];
+ }
+ groups[key].push(process);
+ return groups;
+ }, {}) || {}
+ : {};
return (
- {selectedProcesses?.map((process) => )}
+ {/* Cluster View Toggle */}
+
+
+
+ Cluster View
+
+ {clusterViewEnabled
+ ? "Group processes in the same cluster"
+ : "Show each process separately"
+ }
+
+
+
+
+
+
+
+
+ {/* Process Display */}
+ {clusterViewEnabled ? (
+ // Cluster view: group processes by name
+ Object.entries(processGroups).map(([clusterName, processes]) => {
+ // If there's only one process, show individual ProcessItem
+ if (processes.length === 1) {
+ return ;
+ }
+ // If there are multiple processes with same name, show as cluster
+ return (
+
+ );
+ })
+ ) : (
+ // Individual view: show each process separately
+ selectedProcesses?.map((process) => (
+
+ ))
+ )}
);
}