mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-11-02 04:53:40 +00:00
Added a few dashboard enhancements with Doughnut charts
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"name": "patchmon-frontend",
|
||||
"private": true,
|
||||
"version": "1.2.6",
|
||||
"license": "AGPL-3.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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"]);
|
||||
},
|
||||
|
||||
@@ -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
1827
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
||||
"name": "patchmon",
|
||||
"version": "1.2.6",
|
||||
"description": "Linux Patch Monitoring System",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"backend",
|
||||
|
||||
Reference in New Issue
Block a user