mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-10-23 07:42:05 +00:00
@@ -316,7 +316,7 @@ const Layout = ({ children }) => {
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
aria-label="Close sidebar"
|
||||
/>
|
||||
<div className="relative flex w-full max-w-[280px] flex-col bg-white pb-4 pt-5 shadow-xl">
|
||||
<div className="relative flex w-full max-w-[280px] flex-col bg-white dark:bg-secondary-800 pb-4 pt-5 shadow-xl">
|
||||
<div className="absolute right-0 top-0 -mr-12 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
@@ -352,8 +352,8 @@ const Layout = ({ children }) => {
|
||||
to={item.href}
|
||||
className={`group flex items-center px-2 py-2 text-sm font-medium rounded-md ${
|
||||
isActive(item.href)
|
||||
? "bg-primary-100 text-primary-900"
|
||||
: "text-secondary-600 hover:bg-secondary-50 hover:text-secondary-900"
|
||||
? "bg-primary-100 dark:bg-primary-600 text-primary-900 dark:text-white"
|
||||
: "text-secondary-600 dark:text-secondary-300 hover:bg-secondary-50 dark:hover:bg-secondary-700 hover:text-secondary-900 dark:hover:text-white"
|
||||
}`}
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
>
|
||||
@@ -365,80 +365,82 @@ const Layout = ({ children }) => {
|
||||
// Section with items
|
||||
return (
|
||||
<div key={item.section}>
|
||||
<h3 className="text-xs font-semibold text-secondary-500 uppercase tracking-wider mb-2">
|
||||
<h3 className="text-xs font-semibold text-secondary-500 dark:text-secondary-400 uppercase tracking-wider mb-2">
|
||||
{item.section}
|
||||
</h3>
|
||||
<div className="space-y-1">
|
||||
{item.items.map((subItem) => (
|
||||
<div key={subItem.name}>
|
||||
{subItem.name === "Hosts" && canManageHosts() ? (
|
||||
// Special handling for Hosts item with integrated + button (mobile)
|
||||
<Link
|
||||
to={subItem.href}
|
||||
className={`group flex items-center px-2 py-2 text-sm font-medium rounded-md ${
|
||||
isActive(subItem.href)
|
||||
? "bg-primary-100 text-primary-900"
|
||||
: "text-secondary-600 hover:bg-secondary-50 hover:text-secondary-900"
|
||||
}`}
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
>
|
||||
<subItem.icon className="mr-3 h-5 w-5" />
|
||||
<span className="flex items-center gap-2 flex-1">
|
||||
{subItem.name}
|
||||
{subItem.name === "Hosts" &&
|
||||
stats?.cards?.totalHosts !== undefined && (
|
||||
<span className="ml-2 inline-flex items-center justify-center px-1.5 py-0.5 text-xs rounded bg-secondary-100 text-secondary-700">
|
||||
{stats.cards.totalHosts}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setSidebarOpen(false);
|
||||
handleAddHost();
|
||||
}}
|
||||
className="ml-auto flex items-center justify-center w-5 h-5 rounded-full border-2 border-current opacity-60 hover:opacity-100 transition-all duration-200 self-center"
|
||||
title="Add Host"
|
||||
{item.items
|
||||
.filter((subItem) => !subItem.comingSoon)
|
||||
.map((subItem) => (
|
||||
<div key={subItem.name}>
|
||||
{subItem.name === "Hosts" && canManageHosts() ? (
|
||||
// Special handling for Hosts item with integrated + button (mobile)
|
||||
<Link
|
||||
to={subItem.href}
|
||||
className={`group flex items-center px-2 py-2 text-sm font-medium rounded-md ${
|
||||
isActive(subItem.href)
|
||||
? "bg-primary-100 dark:bg-primary-600 text-primary-900 dark:text-white"
|
||||
: "text-secondary-600 dark:text-secondary-300 hover:bg-secondary-50 dark:hover:bg-secondary-700 hover:text-secondary-900 dark:hover:text-white"
|
||||
}`}
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</button>
|
||||
</Link>
|
||||
) : (
|
||||
// Standard navigation item (mobile)
|
||||
<Link
|
||||
to={subItem.href}
|
||||
className={`group flex items-center px-2 py-2 text-sm font-medium rounded-md ${
|
||||
isActive(subItem.href)
|
||||
? "bg-primary-100 text-primary-900"
|
||||
: "text-secondary-600 hover:bg-secondary-50 hover:text-secondary-900"
|
||||
} ${subItem.comingSoon ? "opacity-50 cursor-not-allowed" : ""}`}
|
||||
onClick={
|
||||
subItem.comingSoon
|
||||
? (e) => e.preventDefault()
|
||||
: () => setSidebarOpen(false)
|
||||
}
|
||||
>
|
||||
<subItem.icon className="mr-3 h-5 w-5" />
|
||||
<span className="flex items-center gap-2">
|
||||
{subItem.name}
|
||||
{subItem.name === "Hosts" &&
|
||||
stats?.cards?.totalHosts !== undefined && (
|
||||
<span className="ml-2 inline-flex items-center justify-center px-1.5 py-0.5 text-xs rounded bg-secondary-100 text-secondary-700">
|
||||
{stats.cards.totalHosts}
|
||||
<subItem.icon className="mr-3 h-5 w-5" />
|
||||
<span className="flex items-center gap-2 flex-1">
|
||||
{subItem.name}
|
||||
{subItem.name === "Hosts" &&
|
||||
stats?.cards?.totalHosts !== undefined && (
|
||||
<span className="ml-2 inline-flex items-center justify-center px-1.5 py-0.5 text-xs rounded bg-secondary-100 dark:bg-secondary-600 text-secondary-700 dark:text-secondary-200">
|
||||
{stats.cards.totalHosts}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setSidebarOpen(false);
|
||||
handleAddHost();
|
||||
}}
|
||||
className="ml-auto flex items-center justify-center w-5 h-5 rounded-full border-2 border-current opacity-60 hover:opacity-100 transition-all duration-200 self-center"
|
||||
title="Add Host"
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</button>
|
||||
</Link>
|
||||
) : (
|
||||
// Standard navigation item (mobile)
|
||||
<Link
|
||||
to={subItem.href}
|
||||
className={`group flex items-center px-2 py-2 text-sm font-medium rounded-md ${
|
||||
isActive(subItem.href)
|
||||
? "bg-primary-100 dark:bg-primary-600 text-primary-900 dark:text-white"
|
||||
: "text-secondary-600 dark:text-secondary-300 hover:bg-secondary-50 dark:hover:bg-secondary-700 hover:text-secondary-900 dark:hover:text-white"
|
||||
} ${subItem.comingSoon ? "opacity-50 cursor-not-allowed" : ""}`}
|
||||
onClick={
|
||||
subItem.comingSoon
|
||||
? (e) => e.preventDefault()
|
||||
: () => setSidebarOpen(false)
|
||||
}
|
||||
>
|
||||
<subItem.icon className="mr-3 h-5 w-5" />
|
||||
<span className="flex items-center gap-2">
|
||||
{subItem.name}
|
||||
{subItem.name === "Hosts" &&
|
||||
stats?.cards?.totalHosts !== undefined && (
|
||||
<span className="ml-2 inline-flex items-center justify-center px-1.5 py-0.5 text-xs rounded bg-secondary-100 dark:bg-secondary-600 text-secondary-700 dark:text-secondary-200">
|
||||
{stats.cards.totalHosts}
|
||||
</span>
|
||||
)}
|
||||
{subItem.comingSoon && (
|
||||
<span className="text-xs bg-secondary-100 dark:bg-secondary-600 text-secondary-600 dark:text-secondary-200 px-1.5 py-0.5 rounded">
|
||||
Soon
|
||||
</span>
|
||||
)}
|
||||
{subItem.comingSoon && (
|
||||
<span className="text-xs bg-secondary-100 text-secondary-600 px-1.5 py-0.5 rounded">
|
||||
Soon
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</span>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -453,32 +455,70 @@ const Layout = ({ children }) => {
|
||||
return (
|
||||
<div key={item.section}>
|
||||
<div className="space-y-1">
|
||||
{item.items.map((subItem) => (
|
||||
<Link
|
||||
key={subItem.name}
|
||||
to={subItem.href}
|
||||
className={`group flex items-center px-2 py-2 text-sm font-medium rounded-md ${
|
||||
isActive(subItem.href)
|
||||
? "bg-primary-100 text-primary-900"
|
||||
: "text-secondary-600 hover:bg-secondary-50 hover:text-secondary-900"
|
||||
}`}
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
>
|
||||
<subItem.icon className="mr-3 h-5 w-5" />
|
||||
<span className="flex items-center gap-2">
|
||||
{subItem.name}
|
||||
{subItem.showUpgradeIcon && (
|
||||
<UpgradeNotificationIcon className="h-3 w-3" />
|
||||
)}
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
{item.items
|
||||
.filter((subItem) => !subItem.comingSoon)
|
||||
.map((subItem) => (
|
||||
<Link
|
||||
key={subItem.name}
|
||||
to={subItem.href}
|
||||
className={`group flex items-center px-2 py-2 text-sm font-medium rounded-md ${
|
||||
isActive(subItem.href)
|
||||
? "bg-primary-100 dark:bg-primary-600 text-primary-900 dark:text-white"
|
||||
: "text-secondary-600 dark:text-secondary-300 hover:bg-secondary-50 dark:hover:bg-secondary-700 hover:text-secondary-900 dark:hover:text-white"
|
||||
}`}
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
>
|
||||
<subItem.icon className="mr-3 h-5 w-5" />
|
||||
<span className="flex items-center gap-2">
|
||||
{subItem.name}
|
||||
{subItem.showUpgradeIcon && (
|
||||
<UpgradeNotificationIcon className="h-3 w-3" />
|
||||
)}
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
|
||||
{/* Mobile Logout Section */}
|
||||
<div className="mt-8 pt-4 border-t border-secondary-200 dark:border-secondary-700">
|
||||
<div className="px-2 space-y-1">
|
||||
<Link
|
||||
to="/settings/profile"
|
||||
className={`group flex items-center px-2 py-2 text-sm font-medium rounded-md ${
|
||||
isActive("/settings/profile")
|
||||
? "bg-primary-100 dark:bg-primary-600 text-primary-900 dark:text-white"
|
||||
: "text-secondary-600 dark:text-secondary-300 hover:bg-secondary-50 dark:hover:bg-secondary-700 hover:text-secondary-900 dark:hover:text-white"
|
||||
}`}
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
>
|
||||
<UserCircle className="mr-3 h-5 w-5" />
|
||||
<span className="flex items-center gap-2">
|
||||
{user?.first_name || user?.username}
|
||||
{user?.role === "admin" && (
|
||||
<span className="text-xs bg-secondary-100 dark:bg-secondary-600 text-secondary-600 dark:text-secondary-200 px-1.5 py-0.5 rounded">
|
||||
Admin
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
handleLogout();
|
||||
setSidebarOpen(false);
|
||||
}}
|
||||
className="w-full group flex items-center px-2 py-2 text-sm font-medium rounded-md text-secondary-600 dark:text-secondary-300 hover:bg-secondary-50 dark:hover:bg-secondary-700 hover:text-secondary-900 dark:hover:text-white"
|
||||
>
|
||||
<LogOut className="mr-3 h-5 w-5" />
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
@@ -879,24 +919,29 @@ const Layout = ({ children }) => {
|
||||
<div className="sticky top-0 z-40 flex h-16 shrink-0 items-center gap-x-4 border-b border-secondary-200 dark:border-secondary-600 bg-white dark:bg-secondary-800 px-4 shadow-sm sm:gap-x-6 sm:px-6 lg:px-8">
|
||||
<button
|
||||
type="button"
|
||||
className="-m-2.5 p-2.5 text-secondary-700 lg:hidden"
|
||||
className="-m-2.5 p-2.5 text-secondary-700 dark:text-white lg:hidden"
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
>
|
||||
<Menu className="h-6 w-6" />
|
||||
</button>
|
||||
|
||||
{/* Separator */}
|
||||
<div className="h-6 w-px bg-secondary-200 lg:hidden" />
|
||||
<div className="h-6 w-px bg-secondary-200 dark:bg-secondary-600 lg:hidden" />
|
||||
|
||||
<div className="flex flex-1 gap-x-4 self-stretch lg:gap-x-6">
|
||||
<div className="relative flex items-center">
|
||||
<h2 className="text-lg font-semibold text-secondary-900 dark:text-secondary-100 whitespace-nowrap">
|
||||
{getPageTitle()}
|
||||
</h2>
|
||||
</div>
|
||||
{/* Page title - hidden on dashboard to give more space to search */}
|
||||
{location.pathname !== "/" && (
|
||||
<div className="relative flex items-center">
|
||||
<h2 className="text-lg font-semibold text-secondary-900 dark:text-secondary-100 whitespace-nowrap">
|
||||
{getPageTitle()}
|
||||
</h2>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Global Search Bar */}
|
||||
<div className="flex items-center max-w-sm">
|
||||
<div
|
||||
className={`flex items-center ${location.pathname === "/" ? "flex-1 max-w-none" : "max-w-sm"}`}
|
||||
>
|
||||
<GlobalSearch />
|
||||
</div>
|
||||
|
||||
|
@@ -1150,23 +1150,46 @@ const Dashboard = () => {
|
||||
callbacks: {
|
||||
title: (context) => {
|
||||
const label = context[0].label;
|
||||
|
||||
// Handle empty or invalid labels
|
||||
if (!label || typeof label !== "string") {
|
||||
return "Unknown Date";
|
||||
}
|
||||
|
||||
// Format hourly labels (e.g., "2025-10-07T14" -> "Oct 7, 2:00 PM")
|
||||
if (label.includes("T")) {
|
||||
const date = new Date(`${label}:00:00`);
|
||||
try {
|
||||
const date = new Date(`${label}:00:00`);
|
||||
// Check if date is valid
|
||||
if (isNaN(date.getTime())) {
|
||||
return label; // Return original label if date is invalid
|
||||
}
|
||||
return date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
});
|
||||
} catch (error) {
|
||||
return label; // Return original label if parsing fails
|
||||
}
|
||||
}
|
||||
|
||||
// Format daily labels (e.g., "2025-10-07" -> "Oct 7")
|
||||
try {
|
||||
const date = new Date(label);
|
||||
// Check if date is valid
|
||||
if (isNaN(date.getTime())) {
|
||||
return label; // Return original label if date is invalid
|
||||
}
|
||||
return date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
});
|
||||
} catch (error) {
|
||||
return label; // Return original label if parsing fails
|
||||
}
|
||||
// Format daily labels (e.g., "2025-10-07" -> "Oct 7")
|
||||
const date = new Date(label);
|
||||
return date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1186,24 +1209,49 @@ const Dashboard = () => {
|
||||
},
|
||||
callback: function (value, _index, _ticks) {
|
||||
const label = this.getLabelForValue(value);
|
||||
|
||||
// Handle empty or invalid labels
|
||||
if (!label || typeof label !== "string") {
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
// Format hourly labels (e.g., "2025-10-07T14" -> "2 PM")
|
||||
if (label.includes("T")) {
|
||||
const hour = label.split("T")[1];
|
||||
const hourNum = parseInt(hour, 10);
|
||||
return hourNum === 0
|
||||
? "12 AM"
|
||||
: hourNum < 12
|
||||
? `${hourNum} AM`
|
||||
: hourNum === 12
|
||||
? "12 PM"
|
||||
: `${hourNum - 12} PM`;
|
||||
try {
|
||||
const hour = label.split("T")[1];
|
||||
const hourNum = parseInt(hour, 10);
|
||||
|
||||
// Validate hour number
|
||||
if (isNaN(hourNum) || hourNum < 0 || hourNum > 23) {
|
||||
return hour; // Return original hour if invalid
|
||||
}
|
||||
|
||||
return hourNum === 0
|
||||
? "12 AM"
|
||||
: hourNum < 12
|
||||
? `${hourNum} AM`
|
||||
: hourNum === 12
|
||||
? "12 PM"
|
||||
: `${hourNum - 12} PM`;
|
||||
} catch (error) {
|
||||
return label; // Return original label if parsing fails
|
||||
}
|
||||
}
|
||||
|
||||
// Format daily labels (e.g., "2025-10-07" -> "Oct 7")
|
||||
const date = new Date(label);
|
||||
return date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
try {
|
||||
const date = new Date(label);
|
||||
// Check if date is valid
|
||||
if (isNaN(date.getTime())) {
|
||||
return label; // Return original label if date is invalid
|
||||
}
|
||||
return date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
} catch (error) {
|
||||
return label; // Return original label if parsing fails
|
||||
}
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
|
@@ -1570,6 +1570,7 @@ const BulkAssignModal = ({
|
||||
isLoading,
|
||||
}) => {
|
||||
const [selectedGroupId, setSelectedGroupId] = useState("");
|
||||
const bulkHostGroupId = useId();
|
||||
|
||||
// Fetch host groups for selection
|
||||
const { data: hostGroups } = useQuery({
|
||||
@@ -1588,28 +1589,31 @@ const BulkAssignModal = ({
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-6 w-full max-w-md">
|
||||
<div className="bg-white dark:bg-secondary-800 rounded-lg p-6 w-full max-w-md">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-semibold text-secondary-900">
|
||||
<h3 className="text-lg font-semibold text-secondary-900 dark:text-white">
|
||||
Assign to Host Group
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="text-secondary-400 hover:text-secondary-600"
|
||||
className="text-secondary-400 hover:text-secondary-600 dark:text-secondary-300 dark:hover:text-secondary-100"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<p className="text-sm text-secondary-600 mb-2">
|
||||
<p className="text-sm text-secondary-600 dark:text-secondary-400 mb-2">
|
||||
Assigning {selectedHosts.length} host
|
||||
{selectedHosts.length !== 1 ? "s" : ""}:
|
||||
</p>
|
||||
<div className="max-h-32 overflow-y-auto bg-secondary-50 rounded-md p-3">
|
||||
<div className="max-h-32 overflow-y-auto bg-secondary-50 dark:bg-secondary-700 rounded-md p-3">
|
||||
{selectedHostNames.map((friendlyName) => (
|
||||
<div key={friendlyName} className="text-sm text-secondary-700">
|
||||
<div
|
||||
key={friendlyName}
|
||||
className="text-sm text-secondary-700 dark:text-secondary-300"
|
||||
>
|
||||
• {friendlyName}
|
||||
</div>
|
||||
))}
|
||||
@@ -1620,7 +1624,7 @@ const BulkAssignModal = ({
|
||||
<div>
|
||||
<label
|
||||
htmlFor={bulkHostGroupId}
|
||||
className="block text-sm font-medium text-secondary-700 mb-1"
|
||||
className="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1"
|
||||
>
|
||||
Host Group
|
||||
</label>
|
||||
@@ -1628,7 +1632,7 @@ const BulkAssignModal = ({
|
||||
id={bulkHostGroupId}
|
||||
value={selectedGroupId}
|
||||
onChange={(e) => setSelectedGroupId(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-secondary-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
className="w-full px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-md bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
>
|
||||
<option value="">No group (ungrouped)</option>
|
||||
{hostGroups?.map((group) => (
|
||||
@@ -1637,7 +1641,7 @@ const BulkAssignModal = ({
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="mt-1 text-sm text-secondary-500">
|
||||
<p className="mt-1 text-sm text-secondary-500 dark:text-secondary-400">
|
||||
Select a group to assign these hosts to, or leave ungrouped.
|
||||
</p>
|
||||
</div>
|
||||
|
@@ -29,12 +29,11 @@ export const getOSIcon = (osType) => {
|
||||
os.includes("rhel") ||
|
||||
os.includes("red hat") ||
|
||||
os.includes("almalinux") ||
|
||||
os.includes("rocky") ||
|
||||
os === "ol" ||
|
||||
os.includes("oraclelinux") ||
|
||||
os.includes("oracle linux")
|
||||
os.includes("rocky")
|
||||
)
|
||||
return SiCentos;
|
||||
if (os === "ol" || os.includes("oraclelinux") || os.includes("oracle linux"))
|
||||
return SiLinux; // Use generic Linux icon for Oracle Linux
|
||||
if (os.includes("fedora")) return SiFedora;
|
||||
if (os.includes("arch")) return SiArchlinux;
|
||||
if (os.includes("alpine")) return SiAlpinelinux;
|
||||
|
Reference in New Issue
Block a user