Added a few dashboard enhancements with Doughnut charts

This commit is contained in:
Muhammad Ibrahim
2025-09-26 02:10:55 +01:00
parent dbebb866b9
commit 0c0446ad69
11 changed files with 150 additions and 1885 deletions

View File

@@ -2,6 +2,7 @@
"name": "patchmon-backend",
"version": "1.2.6",
"description": "Backend API for Linux Patch Monitoring System",
"license": "AGPL-3.0",
"main": "src/server.js",
"scripts": {
"dev": "nodemon src/server.js",

View File

@@ -110,30 +110,35 @@ async function createDefaultDashboardPreferences(userId, userRole = "user") {
requiredPermission: "can_view_reports",
order: 9,
},
{
cardId: "osDistributionDoughnut",
requiredPermission: "can_view_reports",
order: 10,
},
{
cardId: "recentCollection",
requiredPermission: "can_view_hosts",
order: 10,
order: 11,
},
{
cardId: "updateStatus",
requiredPermission: "can_view_reports",
order: 11,
order: 12,
},
{
cardId: "packagePriority",
requiredPermission: "can_view_packages",
order: 12,
order: 13,
},
{
cardId: "recentUsers",
requiredPermission: "can_view_users",
order: 13,
order: 14,
},
{
cardId: "quickStats",
requiredPermission: "can_view_dashboard",
order: 14,
order: 15,
},
];
@@ -308,40 +313,47 @@ router.get("/defaults", authenticateToken, async (_req, res) => {
enabled: true,
order: 9,
},
{
cardId: "osDistributionDoughnut",
title: "OS Distribution (Doughnut)",
icon: "PieChart",
enabled: true,
order: 10,
},
{
cardId: "recentCollection",
title: "Recent Collection",
icon: "Server",
enabled: true,
order: 10,
order: 11,
},
{
cardId: "updateStatus",
title: "Update Status",
icon: "BarChart3",
enabled: true,
order: 11,
order: 12,
},
{
cardId: "packagePriority",
title: "Package Priority",
icon: "BarChart3",
enabled: true,
order: 12,
order: 13,
},
{
cardId: "recentUsers",
title: "Recent Users Logged in",
icon: "Users",
enabled: true,
order: 13,
order: 14,
},
{
cardId: "quickStats",
title: "Quick Stats",
icon: "TrendingUp",
enabled: true,
order: 14,
order: 15,
},
];

View File

@@ -826,26 +826,31 @@ async function getPermissionBasedPreferences(userRole) {
requiredPermission: "can_view_reports",
order: 9,
},
{
cardId: "osDistributionDoughnut",
requiredPermission: "can_view_reports",
order: 10,
},
{
cardId: "recentCollection",
requiredPermission: "can_view_hosts",
order: 10,
order: 11,
}, // Collection is host-related
{
cardId: "updateStatus",
requiredPermission: "can_view_reports",
order: 11,
order: 12,
},
{
cardId: "packagePriority",
requiredPermission: "can_view_packages",
order: 12,
order: 13,
},
{ cardId: "recentUsers", requiredPermission: "can_view_users", order: 13 },
{ cardId: "recentUsers", requiredPermission: "can_view_users", order: 14 },
{
cardId: "quickStats",
requiredPermission: "can_view_dashboard",
order: 14,
order: 15,
},
];

View File

@@ -2,6 +2,7 @@
"name": "patchmon-frontend",
"private": true,
"version": "1.2.6",
"license": "AGPL-3.0",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -171,6 +171,7 @@ const DashboardSettingsModal = ({ isOpen, onClose }) => {
return "Top card";
if (cardId === "osDistribution") return "Pie chart";
if (cardId === "osDistributionBar") return "Bar chart";
if (cardId === "osDistributionDoughnut") return "Doughnut chart";
if (cardId === "updateStatus") return "Pie chart";
if (cardId === "packagePriority") return "Pie chart";
if (cardId === "recentUsers") return "Table";

View File

@@ -1,38 +1,16 @@
import { Check, Edit2, X } from "lucide-react";
import { useEffect, useState } from "react";
import { useState } from "react";
const InlineToggle = ({
value,
onSave,
onCancel,
className = "",
disabled = false,
trueLabel = "Yes",
falseLabel = "No",
}) => {
const [isEditing, setIsEditing] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState("");
useEffect(() => {
// Auto-save when value changes during editing
if (isEditing && !isLoading) {
handleSave(!value);
}
}, [isEditing]);
const handleEdit = () => {
if (disabled) return;
setIsEditing(true);
setError("");
};
const handleCancel = () => {
setIsEditing(false);
setError("");
if (onCancel) onCancel();
};
const handleSave = async (newValue) => {
if (disabled || isLoading) return;
@@ -95,9 +73,7 @@ const InlineToggle = ({
</button>
)}
{error && (
<span className="text-xs text-red-600 dark:text-red-400">
{error}
</span>
<span className="text-xs text-red-600 dark:text-red-400">{error}</span>
)}
</div>
);

View File

@@ -23,7 +23,7 @@ import {
WifiOff,
} from "lucide-react";
import { useEffect, useState } from "react";
import { Bar, Pie } from "react-chartjs-2";
import { Bar, Doughnut, Pie } from "react-chartjs-2";
import { useNavigate } from "react-router-dom";
import DashboardSettingsModal from "../components/DashboardSettingsModal";
import { useAuth } from "../contexts/AuthContext";
@@ -291,6 +291,7 @@ const Dashboard = () => {
[
"osDistribution",
"osDistributionBar",
"osDistributionDoughnut",
"updateStatus",
"packagePriority",
"recentUsers",
@@ -664,8 +665,34 @@ const Dashboard = () => {
<h3 className="text-lg font-medium text-secondary-900 dark:text-white mb-4">
OS Distribution
</h3>
<div className="h-64">
<Pie data={osChartData} options={chartOptions} />
<div className="h-64 w-full flex items-center justify-center">
<div className="w-full h-full max-w-sm">
<Pie data={osChartData} options={chartOptions} />
</div>
</div>
</button>
);
case "osDistributionDoughnut":
return (
<button
type="button"
className="card p-6 cursor-pointer hover:shadow-card-hover dark:hover:shadow-card-hover-dark transition-shadow duration-200 w-full text-left"
onClick={handleOSDistributionClick}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleOSDistributionClick();
}
}}
>
<h3 className="text-lg font-medium text-secondary-900 dark:text-white mb-4">
OS Distribution
</h3>
<div className="h-64 w-full flex items-center justify-center">
<div className="w-full h-full max-w-sm">
<Doughnut data={osChartData} options={doughnutChartOptions} />
</div>
</div>
</button>
);
@@ -708,11 +735,13 @@ const Dashboard = () => {
<h3 className="text-lg font-medium text-secondary-900 dark:text-white mb-4">
Update Status
</h3>
<div className="h-64">
<Pie
data={updateStatusChartData}
options={updateStatusChartOptions}
/>
<div className="h-64 w-full flex items-center justify-center">
<div className="w-full h-full max-w-sm">
<Pie
data={updateStatusChartData}
options={updateStatusChartOptions}
/>
</div>
</div>
</button>
);
@@ -733,11 +762,13 @@ const Dashboard = () => {
<h3 className="text-lg font-medium text-secondary-900 dark:text-white mb-4">
Package Priority
</h3>
<div className="h-64">
<Pie
data={packagePriorityChartData}
options={packagePriorityChartOptions}
/>
<div className="h-64 w-full flex items-center justify-center">
<div className="w-full h-full max-w-sm">
<Pie
data={packagePriorityChartData}
options={packagePriorityChartOptions}
/>
</div>
</div>
</button>
);
@@ -875,9 +906,13 @@ const Dashboard = () => {
key={host.id}
className="flex items-center justify-between py-2 border-b border-secondary-100 dark:border-secondary-700 last:border-b-0"
>
<div className="text-sm font-medium text-secondary-900 dark:text-white">
<button
type="button"
onClick={() => navigate(`/hosts/${host.id}`)}
className="text-sm font-medium text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 hover:underline text-left"
>
{host.friendly_name || host.hostname}
</div>
</button>
<div className="text-sm text-secondary-500 dark:text-secondary-400">
{host.last_update
? formatRelativeTime(host.last_update)
@@ -935,49 +970,101 @@ const Dashboard = () => {
const chartOptions = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: "bottom",
position: "right",
labels: {
color: isDark ? "#ffffff" : "#374151",
font: {
size: 12,
},
padding: 15,
usePointStyle: true,
pointStyle: "circle",
},
},
},
layout: {
padding: {
right: 20,
},
},
onClick: handleOSChartClick,
};
const doughnutChartOptions = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: "right",
labels: {
color: isDark ? "#ffffff" : "#374151",
font: {
size: 12,
},
padding: 15,
usePointStyle: true,
pointStyle: "circle",
},
},
},
layout: {
padding: {
right: 20,
},
},
onClick: handleOSChartClick,
};
const updateStatusChartOptions = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: "bottom",
position: "right",
labels: {
color: isDark ? "#ffffff" : "#374151",
font: {
size: 12,
},
padding: 15,
usePointStyle: true,
pointStyle: "circle",
},
},
},
layout: {
padding: {
right: 20,
},
},
onClick: handleUpdateStatusChartClick,
};
const packagePriorityChartOptions = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: "bottom",
position: "right",
labels: {
color: isDark ? "#ffffff" : "#374151",
font: {
size: 12,
},
padding: 15,
usePointStyle: true,
pointStyle: "circle",
},
},
},
layout: {
padding: {
right: 20,
},
},
onClick: handlePackagePriorityChartClick,
};

View File

@@ -474,7 +474,9 @@ const Hosts = () => {
const toggleAutoUpdateMutation = useMutation({
mutationFn: ({ hostId, autoUpdate }) =>
adminHostsAPI.toggleAutoUpdate(hostId, autoUpdate).then((res) => res.data),
adminHostsAPI
.toggleAutoUpdate(hostId, autoUpdate)
.then((res) => res.data),
onSuccess: () => {
queryClient.invalidateQueries(["hosts"]);
},

View File

@@ -11,7 +11,7 @@ export default defineConfig({
allowedHosts: true, // Allow all hosts in development
proxy: {
"/api": {
target: `http://${process.env.BACKEND_HOST || 'localhost'}:${process.env.BACKEND_PORT || '3001'}`,
target: `http://${process.env.BACKEND_HOST || "localhost"}:${process.env.BACKEND_PORT || "3001"}`,
changeOrigin: true,
secure: false,
configure:

1827
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@
"name": "patchmon",
"version": "1.2.6",
"description": "Linux Patch Monitoring System",
"license": "AGPL-3.0",
"private": true,
"workspaces": [
"backend",